www.mooseframework.org
AutomaticMortarGeneration.C
Go to the documentation of this file.
1 //* This file is part of the MOOSE framework
2 //* https://www.mooseframework.org
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_quad4.h"
32 #include "libmesh/exodusII_io.h"
33 #include "libmesh/quadrature_gauss.h"
34 #include "libmesh/quadrature_nodal.h"
35 #include "libmesh/distributed_mesh.h"
36 #include "libmesh/replicated_mesh.h"
37 #include "libmesh/enum_to_string.h"
38 #include "libmesh/statistics.h"
39 #include "libmesh/equation_systems.h"
40 
41 #include "metaphysicl/dualnumber.h"
42 
43 #include "timpi/communicator.h"
44 #include "timpi/parallel_sync.h"
45 
46 #include <array>
47 #include <algorithm>
48 
49 using namespace libMesh;
51 
52 // Make newer nanoflann API spelling compatible with older nanoflann
53 // versions
54 #if NANOFLANN_VERSION < 0x150
55 namespace nanoflann
56 {
57 typedef SearchParams SearchParameters;
58 }
59 #endif
60 
62 {
63 public:
65  {
66  auto params = Output::validParams();
67  params.addPrivateParam<AutomaticMortarGeneration *>("_amg", nullptr);
68  params.addPrivateParam<MooseApp *>("_moose_app", nullptr);
69  params.set<std::string>("_type") = "MortarNodalGeometryOutput";
70  return params;
71  };
72 
74  : Output(params), _amg(*getCheckedPointerParam<AutomaticMortarGeneration *>("_amg"))
75  {
76  }
77 
78  void output() override
79  {
80  // Must call compute_nodal_geometry first!
81  if (_amg._secondary_node_to_nodal_normal.empty() ||
82  _amg._secondary_node_to_hh_nodal_tangents.empty())
83  mooseError("No entries found in the secondary node -> nodal geometry map.");
84 
85  auto & problem = _app.feProblem();
86  auto & subproblem = _amg._on_displaced
87  ? static_cast<SubProblem &>(*problem.getDisplacedProblem())
88  : static_cast<SubProblem &>(problem);
89  auto & nodal_normals_es = subproblem.es();
90 
91  const std::string nodal_normals_sys_name = "nodal_normals";
92 
93  if (!_nodal_normals_system)
94  {
95  for (const auto s : make_range(nodal_normals_es.n_systems()))
96  if (!nodal_normals_es.get_system(s).is_initialized())
97  // This is really early on in the simulation and the systems have not been initialized. We
98  // thus need to avoid calling reinit on systems that haven't even had their first init yet
99  return;
100 
101  _nodal_normals_system =
102  &nodal_normals_es.template add_system<ExplicitSystem>(nodal_normals_sys_name);
103  _nnx_var_num = _nodal_normals_system->add_variable("nodal_normal_x", FEType(FIRST, LAGRANGE)),
104  _nny_var_num = _nodal_normals_system->add_variable("nodal_normal_y", FEType(FIRST, LAGRANGE));
105  _nnz_var_num = _nodal_normals_system->add_variable("nodal_normal_z", FEType(FIRST, LAGRANGE));
106 
107  _t1x_var_num =
108  _nodal_normals_system->add_variable("nodal_tangent_1_x", FEType(FIRST, LAGRANGE)),
109  _t1y_var_num =
110  _nodal_normals_system->add_variable("nodal_tangent_1_y", FEType(FIRST, LAGRANGE));
111  _t1z_var_num =
112  _nodal_normals_system->add_variable("nodal_tangent_1_z", FEType(FIRST, LAGRANGE));
113 
114  _t2x_var_num =
115  _nodal_normals_system->add_variable("nodal_tangent_2_x", FEType(FIRST, LAGRANGE)),
116  _t2y_var_num =
117  _nodal_normals_system->add_variable("nodal_tangent_2_y", FEType(FIRST, LAGRANGE));
118  _t2z_var_num =
119  _nodal_normals_system->add_variable("nodal_tangent_2_z", FEType(FIRST, LAGRANGE));
120  nodal_normals_es.reinit();
121  }
122 
123  const DofMap & dof_map = _nodal_normals_system->get_dof_map();
124  std::vector<dof_id_type> dof_indices_nnx, dof_indices_nny, dof_indices_nnz;
125  std::vector<dof_id_type> dof_indices_t1x, dof_indices_t1y, dof_indices_t1z;
126  std::vector<dof_id_type> dof_indices_t2x, dof_indices_t2y, dof_indices_t2z;
127 
128  for (MeshBase::const_element_iterator el = _amg._mesh.elements_begin(),
129  end_el = _amg._mesh.elements_end();
130  el != end_el;
131  ++el)
132  {
133  const Elem * elem = *el;
134 
135  // Get the nodal dofs for this Elem.
136  dof_map.dof_indices(elem, dof_indices_nnx, _nnx_var_num);
137  dof_map.dof_indices(elem, dof_indices_nny, _nny_var_num);
138  dof_map.dof_indices(elem, dof_indices_nnz, _nnz_var_num);
139 
140  dof_map.dof_indices(elem, dof_indices_t1x, _t1x_var_num);
141  dof_map.dof_indices(elem, dof_indices_t1y, _t1y_var_num);
142  dof_map.dof_indices(elem, dof_indices_t1z, _t1z_var_num);
143 
144  dof_map.dof_indices(elem, dof_indices_t2x, _t2x_var_num);
145  dof_map.dof_indices(elem, dof_indices_t2y, _t2y_var_num);
146  dof_map.dof_indices(elem, dof_indices_t2z, _t2z_var_num);
147 
148  //
149 
150  // For each node of the Elem, if it is in the secondary_node_to_nodal_normal
151  // container, set the corresponding nodal normal dof values.
152  for (MooseIndex(elem->n_vertices()) n = 0; n < elem->n_vertices(); ++n)
153  {
154  auto it = _amg._secondary_node_to_nodal_normal.find(elem->node_ptr(n));
155  if (it != _amg._secondary_node_to_nodal_normal.end())
156  {
157  _nodal_normals_system->solution->set(dof_indices_nnx[n], it->second(0));
158  _nodal_normals_system->solution->set(dof_indices_nny[n], it->second(1));
159  _nodal_normals_system->solution->set(dof_indices_nnz[n], it->second(2));
160  }
161 
162  auto it_tangent = _amg._secondary_node_to_hh_nodal_tangents.find(elem->node_ptr(n));
163  if (it_tangent != _amg._secondary_node_to_hh_nodal_tangents.end())
164  {
165  _nodal_normals_system->solution->set(dof_indices_t1x[n], it_tangent->second[0](0));
166  _nodal_normals_system->solution->set(dof_indices_t1y[n], it_tangent->second[0](1));
167  _nodal_normals_system->solution->set(dof_indices_t1z[n], it_tangent->second[0](2));
168 
169  _nodal_normals_system->solution->set(dof_indices_t2x[n], it_tangent->second[1](0));
170  _nodal_normals_system->solution->set(dof_indices_t2y[n], it_tangent->second[1](1));
171  _nodal_normals_system->solution->set(dof_indices_t2z[n], it_tangent->second[1](2));
172  }
173 
174  } // end loop over nodes
175  } // end loop over elems
176 
177  // Finish assembly.
178  _nodal_normals_system->solution->close();
179 
180  std::set<std::string> sys_names = {nodal_normals_sys_name};
181 
182  // Write the nodal normals to file
183  ExodusII_IO nodal_normals_writer(_amg._mesh);
184 
185  // Default to non-HDF5 output for wider compatibility
186  nodal_normals_writer.set_hdf5_writing(false);
187 
188  nodal_normals_writer.write_equation_systems(
189  "nodal_geometry_only.e", nodal_normals_es, &sys_names);
190  }
191 
192 private:
195 
197 
198  libMesh::System * _nodal_normals_system = nullptr;
199  unsigned int _nnx_var_num;
200  unsigned int _nny_var_num;
201  unsigned int _nnz_var_num;
202 
203  unsigned int _t1x_var_num;
204  unsigned int _t1y_var_num;
205  unsigned int _t1z_var_num;
206 
207  unsigned int _t2x_var_num;
208  unsigned int _t2y_var_num;
209  unsigned int _t2z_var_num;
211 };
212 
214  MooseApp & app,
215  MeshBase & mesh_in,
216  const std::pair<BoundaryID, BoundaryID> & boundary_key,
217  const std::pair<SubdomainID, SubdomainID> & subdomain_key,
218  bool on_displaced,
219  bool periodic,
220  const bool debug,
221  const bool correct_edge_dropping,
222  const Real minimum_projection_angle)
223  : ConsoleStreamInterface(app),
224  _app(app),
225  _mesh(mesh_in),
226  _debug(debug),
227  _on_displaced(on_displaced),
228  _periodic(periodic),
229  _distributed(!_mesh.is_replicated()),
230  _correct_edge_dropping(correct_edge_dropping),
231  _minimum_projection_angle(minimum_projection_angle)
232 {
233  _primary_secondary_boundary_id_pairs.push_back(boundary_key);
234  _primary_requested_boundary_ids.insert(boundary_key.first);
235  _secondary_requested_boundary_ids.insert(boundary_key.second);
236  _primary_secondary_subdomain_id_pairs.push_back(subdomain_key);
237  _primary_boundary_subdomain_ids.insert(subdomain_key.first);
238  _secondary_boundary_subdomain_ids.insert(subdomain_key.second);
239 
240  if (_distributed)
241  _mortar_segment_mesh = std::make_unique<DistributedMesh>(_mesh.comm());
242  else
243  _mortar_segment_mesh = std::make_unique<ReplicatedMesh>(_mesh.comm());
244 }
245 
246 void
248 {
249  if (!_debug)
250  return;
251 
252  _output_params = std::make_unique<InputParameters>(MortarNodalGeometryOutput::validParams());
253  _output_params->set<AutomaticMortarGeneration *>("_amg") = this;
254  _output_params->set<FEProblemBase *>("_fe_problem_base") = &_app.feProblem();
255  _output_params->set<MooseApp *>("_moose_app") = &_app;
256  _output_params->set<std::string>("_object_name") =
257  "mortar_nodal_geometry_" +
258  std::to_string(_primary_secondary_boundary_id_pairs.front().first) +
259  std::to_string(_primary_secondary_boundary_id_pairs.front().second) + "_" +
260  (_on_displaced ? "displaced" : "undisplaced");
261  _output_params->finalize("MortarNodalGeometryOutput");
262  _app.getOutputWarehouse().addOutput(std::make_shared<MortarNodalGeometryOutput>(*_output_params));
263 }
264 
265 void
267 {
268  _mortar_segment_mesh->clear();
273  _msm_elem_to_info.clear();
274  _lower_elem_to_side_id.clear();
280  _secondary_ip_sub_ids.clear();
281  _primary_ip_sub_ids.clear();
282 }
283 
284 void
286 {
288  mooseError(
289  "Must specify secondary and primary boundary ids before building node-to-elem maps.");
290 
291  // Construct nodes_to_secondary_elem_map
292  for (const auto & secondary_elem :
293  as_range(_mesh.active_elements_begin(), _mesh.active_elements_end()))
294  {
295  // If this is not one of the lower-dimensional secondary side elements, go on to the next one.
296  if (!this->_secondary_boundary_subdomain_ids.count(secondary_elem->subdomain_id()))
297  continue;
298 
299  for (const auto & nd : secondary_elem->node_ref_range())
300  {
301  std::vector<const Elem *> & vec = _nodes_to_secondary_elem_map[nd.id()];
302  vec.push_back(secondary_elem);
303  }
304  }
305 
306  // Construct nodes_to_primary_elem_map
307  for (const auto & primary_elem :
308  as_range(_mesh.active_elements_begin(), _mesh.active_elements_end()))
309  {
310  // If this is not one of the lower-dimensional primary side elements, go on to the next one.
311  if (!this->_primary_boundary_subdomain_ids.count(primary_elem->subdomain_id()))
312  continue;
313 
314  for (const auto & nd : primary_elem->node_ref_range())
315  {
316  std::vector<const Elem *> & vec = _nodes_to_primary_elem_map[nd.id()];
317  vec.push_back(primary_elem);
318  }
319  }
320 }
321 
322 std::vector<Point>
324 {
325  std::vector<Point> nodal_normals(secondary_elem.n_nodes());
326  for (const auto n : make_range(secondary_elem.n_nodes()))
327  nodal_normals[n] = _secondary_node_to_nodal_normal.at(secondary_elem.node_ptr(n));
328 
329  return nodal_normals;
330 }
331 
332 const Elem *
334  dof_id_type secondary_elem_id) const
335 {
336  mooseAssert(_secondary_element_to_secondary_lowerd_element.count(secondary_elem_id),
337  "Map should locate secondary element");
338 
339  return _secondary_element_to_secondary_lowerd_element.at(secondary_elem_id);
340 }
341 
342 std::map<unsigned int, unsigned int>
344 {
345  std::map<unsigned int, unsigned int> secondary_ip_i_to_lower_secondary_i;
346  const Elem * const secondary_ip = lower_secondary_elem.interior_parent();
347  mooseAssert(secondary_ip, "This should be non-null");
348 
349  for (const auto i : make_range(lower_secondary_elem.n_nodes()))
350  {
351  const auto & nd = lower_secondary_elem.node_ref(i);
352  secondary_ip_i_to_lower_secondary_i[secondary_ip->get_node_index(&nd)] = i;
353  }
354 
355  return secondary_ip_i_to_lower_secondary_i;
356 }
357 
358 std::map<unsigned int, unsigned int>
360  const Elem & lower_primary_elem,
361  const Elem & primary_elem,
362  const Elem & /*lower_secondary_elem*/) const
363 {
364  std::map<unsigned int, unsigned int> primary_ip_i_to_lower_primary_i;
365 
366  for (const auto i : make_range(lower_primary_elem.n_nodes()))
367  {
368  const auto & nd = lower_primary_elem.node_ref(i);
369  primary_ip_i_to_lower_primary_i[primary_elem.get_node_index(&nd)] = i;
370  }
371 
372  return primary_ip_i_to_lower_primary_i;
373 }
374 
375 std::array<MooseUtils::SemidynamicVector<Point, 9>, 2>
377 {
378  // MetaPhysicL will check if we ran out of allocated space.
379  MooseUtils::SemidynamicVector<Point, 9> nodal_tangents_one(0);
380  MooseUtils::SemidynamicVector<Point, 9> nodal_tangents_two(0);
381 
382  for (const auto n : make_range(secondary_elem.n_nodes()))
383  {
384  const auto & tangent_vectors =
385  libmesh_map_find(_secondary_node_to_hh_nodal_tangents, secondary_elem.node_ptr(n));
386  nodal_tangents_one.push_back(tangent_vectors[0]);
387  nodal_tangents_two.push_back(tangent_vectors[1]);
388  }
389 
390  return {{nodal_tangents_one, nodal_tangents_two}};
391 }
392 
393 std::vector<Point>
395  const std::vector<Real> & oned_xi1_pts) const
396 {
397  std::vector<Point> xi1_pts(oned_xi1_pts.size());
398  for (const auto qp : index_range(oned_xi1_pts))
399  xi1_pts[qp] = oned_xi1_pts[qp];
400 
401  return getNormals(secondary_elem, xi1_pts);
402 }
403 
404 std::vector<Point>
406  const std::vector<Point> & xi1_pts) const
407 {
408  const auto mortar_dim = _mesh.mesh_dimension() - 1;
409  const auto num_qps = xi1_pts.size();
410  const auto nodal_normals = getNodalNormals(secondary_elem);
411  std::vector<Point> normals(num_qps);
412 
413  for (const auto n : make_range(secondary_elem.n_nodes()))
414  for (const auto qp : make_range(num_qps))
415  {
416  const auto phi =
417  (mortar_dim == 1)
418  ? Moose::fe_lagrange_1D_shape(secondary_elem.default_order(), n, xi1_pts[qp](0))
419  : Moose::fe_lagrange_2D_shape(secondary_elem.type(),
420  secondary_elem.default_order(),
421  n,
422  static_cast<const TypeVector<Real> &>(xi1_pts[qp]));
423  normals[qp] += phi * nodal_normals[n];
424  }
425 
426  if (_periodic)
427  for (auto & normal : normals)
428  normal *= -1;
429 
430  return normals;
431 }
432 
433 void
435 {
436  dof_id_type local_id_index = 0;
437  std::size_t node_unique_id_offset = 0;
438 
439  // Create an offset by the maximum number of mortar segment elements that can be created *plus*
440  // the number of lower-dimensional secondary subdomain elements. Recall that the number of mortar
441  // segments created is a function of node projection, *and* that if we split elems we will delete
442  // that elem which has already taken a unique id
443  for (const auto & pr : _primary_secondary_boundary_id_pairs)
444  {
445  const auto primary_bnd_id = pr.first;
446  const auto secondary_bnd_id = pr.second;
447  const auto num_primary_nodes =
448  std::distance(_mesh.bid_nodes_begin(primary_bnd_id), _mesh.bid_nodes_end(primary_bnd_id));
449  const auto num_secondary_nodes = std::distance(_mesh.bid_nodes_begin(secondary_bnd_id),
450  _mesh.bid_nodes_end(secondary_bnd_id));
451  mooseAssert(num_primary_nodes,
452  "There are no primary nodes on boundary ID "
453  << primary_bnd_id << ". Does that bondary ID even exist on the mesh?");
454  mooseAssert(num_secondary_nodes,
455  "There are no secondary nodes on boundary ID "
456  << secondary_bnd_id << ". Does that bondary ID even exist on the mesh?");
457 
458  node_unique_id_offset += num_primary_nodes + 2 * num_secondary_nodes;
459  }
460 
461  // 1.) Add all lower-dimensional secondary side elements as the "initial" mortar segments.
462  for (MeshBase::const_element_iterator el = _mesh.active_elements_begin(),
463  end_el = _mesh.active_elements_end();
464  el != end_el;
465  ++el)
466  {
467  const Elem * secondary_elem = *el;
468 
469  // If this is not one of the lower-dimensional secondary side elements, go on to the next one.
470  if (!this->_secondary_boundary_subdomain_ids.count(secondary_elem->subdomain_id()))
471  continue;
472 
473  std::vector<Node *> new_nodes;
474  for (MooseIndex(secondary_elem->n_nodes()) n = 0; n < secondary_elem->n_nodes(); ++n)
475  {
476  new_nodes.push_back(_mortar_segment_mesh->add_point(
477  secondary_elem->point(n), secondary_elem->node_id(n), secondary_elem->processor_id()));
478  Node * const new_node = new_nodes.back();
479  new_node->set_unique_id(new_node->id() + node_unique_id_offset);
480  }
481 
482  std::unique_ptr<Elem> new_elem;
483  if (secondary_elem->default_order() == SECOND)
484  new_elem = std::make_unique<Edge3>();
485  else
486  new_elem = std::make_unique<Edge2>();
487 
488  new_elem->processor_id() = secondary_elem->processor_id();
489  new_elem->subdomain_id() = secondary_elem->subdomain_id();
490  new_elem->set_id(local_id_index++);
491  new_elem->set_unique_id(new_elem->id());
492 
493  for (MooseIndex(new_elem->n_nodes()) n = 0; n < new_elem->n_nodes(); ++n)
494  new_elem->set_node(n) = new_nodes[n];
495 
496  Elem * new_elem_ptr = _mortar_segment_mesh->add_elem(new_elem.release());
497 
498  // The xi^(1) values for this mortar segment are initially -1 and 1.
499  MortarSegmentInfo msinfo;
500  msinfo.xi1_a = -1;
501  msinfo.xi1_b = +1;
502  msinfo.secondary_elem = secondary_elem;
503 
504  auto new_container_it0 = _secondary_node_and_elem_to_xi2_primary_elem.find(
505  std::make_pair(secondary_elem->node_ptr(0), secondary_elem)),
506  new_container_it1 = _secondary_node_and_elem_to_xi2_primary_elem.find(
507  std::make_pair(secondary_elem->node_ptr(1), secondary_elem));
508 
509  bool new_container_node0_found =
510  (new_container_it0 != _secondary_node_and_elem_to_xi2_primary_elem.end()),
511  new_container_node1_found =
512  (new_container_it1 != _secondary_node_and_elem_to_xi2_primary_elem.end());
513 
514  const Elem * node0_primary_candidate = nullptr;
515  const Elem * node1_primary_candidate = nullptr;
516 
517  if (new_container_node0_found)
518  {
519  const auto & xi2_primary_elem_pair = new_container_it0->second;
520  msinfo.xi2_a = xi2_primary_elem_pair.first;
521  node0_primary_candidate = xi2_primary_elem_pair.second;
522  }
523 
524  if (new_container_node1_found)
525  {
526  const auto & xi2_primary_elem_pair = new_container_it1->second;
527  msinfo.xi2_b = xi2_primary_elem_pair.first;
528  node1_primary_candidate = xi2_primary_elem_pair.second;
529  }
530 
531  // If both node0 and node1 agree on the primary element they are
532  // projected into, then this mortar segment fits entirely within
533  // a single primary element, and we can go ahead and set the
534  // msinfo.primary_elem pointer now.
535  if (node0_primary_candidate == node1_primary_candidate)
536  msinfo.primary_elem = node0_primary_candidate;
537 
538  // Associate this MSM elem with the MortarSegmentInfo.
539  _msm_elem_to_info.emplace(new_elem_ptr, msinfo);
540 
541  // Maintain the mapping between secondary elems and mortar segment elems contained within them.
542  // Initially, only the original secondary_elem is present.
543  _secondary_elems_to_mortar_segments[secondary_elem->id()].insert(new_elem_ptr);
544  }
545 
546  // 2.) Insert new nodes from primary side and split mortar segments as necessary.
547  for (const auto & pr : _primary_node_and_elem_to_xi1_secondary_elem)
548  {
549  auto key = pr.first;
550  auto val = pr.second;
551 
552  const Node * primary_node = std::get<1>(key);
553  Real xi1 = val.first;
554  const Elem * secondary_elem = val.second;
555 
556  // If this is an aligned node, we don't need to do anything.
557  if (std::abs(std::abs(xi1) - 1.) < _xi_tolerance)
558  continue;
559 
560  auto && order = secondary_elem->default_order();
561 
562  // Determine physical location of new point to be inserted.
563  Point new_pt(0);
564  for (MooseIndex(secondary_elem->n_nodes()) n = 0; n < secondary_elem->n_nodes(); ++n)
565  new_pt += Moose::fe_lagrange_1D_shape(order, n, xi1) * secondary_elem->point(n);
566 
567  // Find the current mortar segment that will have to be split.
568  auto & mortar_segment_set = _secondary_elems_to_mortar_segments[secondary_elem->id()];
569  Elem * current_mortar_segment = nullptr;
570  MortarSegmentInfo * info = nullptr;
571 
572  for (const auto & mortar_segment_candidate : mortar_segment_set)
573  {
574  try
575  {
576  info = &_msm_elem_to_info.at(mortar_segment_candidate);
577  }
578  catch (std::out_of_range &)
579  {
580  mooseError("MortarSegmentInfo not found for the mortar segment candidate");
581  }
582  if (info->xi1_a <= xi1 && xi1 <= info->xi1_b)
583  {
584  current_mortar_segment = mortar_segment_candidate;
585  break;
586  }
587  }
588 
589  // Make sure we found one.
590  if (current_mortar_segment == nullptr)
591  mooseError("Unable to find appropriate mortar segment during linear search!");
592 
593  // If node lands on endpoint of segment, don't split.
594  // Jacob: This condition was getting missed by the < comparison a few lines above. To fix it I
595  // just made it <= and put this condition in to handle equality different. It probably could be
596  // done with a tolerance but the the toleranced equality is already handled later when we drop
597  // segments with small volume.
598  if (info->xi1_a == xi1 || xi1 == info->xi1_b)
599  continue;
600 
601  const auto new_id = _mortar_segment_mesh->max_node_id() + 1;
602  mooseAssert(_mortar_segment_mesh->comm().verify(new_id),
603  "new_id must be the same on all processes");
604  Node * const new_node =
605  _mortar_segment_mesh->add_point(new_pt, new_id, secondary_elem->processor_id());
606  new_node->set_unique_id(new_id + node_unique_id_offset);
607 
608  // Reconstruct the nodal normal at xi1. This will help us
609  // determine the orientation of the primary elems relative to the
610  // new mortar segments.
611  const Point normal = getNormals(*secondary_elem, std::vector<Real>({xi1}))[0];
612 
613  // Get the set of primary_node neighbors.
614  if (this->_nodes_to_primary_elem_map.find(primary_node->id()) ==
615  this->_nodes_to_primary_elem_map.end())
616  mooseError("We should already have built this primary node to elem pair!");
617  const std::vector<const Elem *> & primary_node_neighbors =
618  this->_nodes_to_primary_elem_map[primary_node->id()];
619 
620  // Sanity check
621  if (primary_node_neighbors.size() == 0 || primary_node_neighbors.size() > 2)
622  mooseError("We must have either 1 or 2 primary side nodal neighbors, but we had ",
623  primary_node_neighbors.size());
624 
625  // Primary Elem pointers which we will eventually assign to the
626  // mortar segments being created. We start by assuming
627  // primary_node_neighbor[0] is on the "left" and
628  // primary_node_neighbor[1]/"nothing" is on the "right" and then
629  // swap them if that's not the case.
630  const Elem * left_primary_elem = primary_node_neighbors[0];
631  const Elem * right_primary_elem =
632  (primary_node_neighbors.size() == 2) ? primary_node_neighbors[1] : nullptr;
633 
635 
636  // Storage for z-component of cross products for determining
637  // orientation.
638  std::array<Real, 2> secondary_node_cps;
639  std::vector<Real> primary_node_cps(primary_node_neighbors.size());
640 
641  // Store z-component of left and right secondary node cross products with the nodal normal.
642  for (unsigned int nid = 0; nid < 2; ++nid)
643  secondary_node_cps[nid] = normal.cross(secondary_elem->point(nid) - new_pt)(2);
644 
645  for (MooseIndex(primary_node_neighbors) mnn = 0; mnn < primary_node_neighbors.size(); ++mnn)
646  {
647  const Elem * primary_neigh = primary_node_neighbors[mnn];
648  Point opposite = (primary_neigh->node_ptr(0) == primary_node) ? primary_neigh->point(1)
649  : primary_neigh->point(0);
650  Point cp = normal.cross(opposite - new_pt);
651  primary_node_cps[mnn] = cp(2);
652  }
653 
654  // We will verify that only 1 orientation is actually valid.
655  bool orientation1_valid = false, orientation2_valid = false;
656 
657  if (primary_node_neighbors.size() == 2)
658  {
659  // 2 primary neighbor case
660  orientation1_valid = (secondary_node_cps[0] * primary_node_cps[0] > 0.) &&
661  (secondary_node_cps[1] * primary_node_cps[1] > 0.);
662 
663  orientation2_valid = (secondary_node_cps[0] * primary_node_cps[1] > 0.) &&
664  (secondary_node_cps[1] * primary_node_cps[0] > 0.);
665  }
666  else if (primary_node_neighbors.size() == 1)
667  {
668  // 1 primary neighbor case
669  orientation1_valid = (secondary_node_cps[0] * primary_node_cps[0] > 0.);
670  orientation2_valid = (secondary_node_cps[1] * primary_node_cps[0] > 0.);
671  }
672  else
673  mooseError("Invalid primary node neighbors size ", primary_node_neighbors.size());
674 
675  // Verify that both orientations are not simultaneously valid/invalid. If they are not, then we
676  // are going to throw an exception instead of erroring out since we can easily reach this point
677  // if we have one bad linear solve. It's better in general to catch the error and then try a
678  // smaller time-step
679  if (orientation1_valid && orientation2_valid)
680  throw MooseException(
681  "AutomaticMortarGeneration: Both orientations cannot simultaneously be valid.");
682 
683  // We are going to treat the case where both orientations are invalid as a case in which we
684  // should not be splitting the mortar mesh to incorporate primary mesh elements.
685  // In practice, this case has appeared for very oblique projections, so we assume these cases
686  // will not be considered in mortar thermomechanical contact.
687  if (!orientation1_valid && !orientation2_valid)
688  {
689  mooseDoOnce(mooseWarning(
690  "AutomaticMortarGeneration: Unable to determine valid secondary-primary orientation. "
691  "Consequently we will consider projection of the primary node invalid and not split the "
692  "mortar segment. "
693  "This situation can indicate there are very oblique projections between primary (mortar) "
694  "and secondary (non-mortar) surfaces for a good problem set up. It can also mean your "
695  "time step is too large. This message is only printed once."));
696  continue;
697  }
698 
699  // Make an Elem on the left
700  std::unique_ptr<Elem> new_elem_left;
701  if (order == SECOND)
702  new_elem_left = std::make_unique<Edge3>();
703  else
704  new_elem_left = std::make_unique<Edge2>();
705 
706  new_elem_left->processor_id() = current_mortar_segment->processor_id();
707  new_elem_left->subdomain_id() = current_mortar_segment->subdomain_id();
708  new_elem_left->set_id(local_id_index++);
709  new_elem_left->set_unique_id(new_elem_left->id());
710  new_elem_left->set_node(0) = current_mortar_segment->node_ptr(0);
711  new_elem_left->set_node(1) = new_node;
712 
713  // Make an Elem on the right
714  std::unique_ptr<Elem> new_elem_right;
715  if (order == SECOND)
716  new_elem_right = std::make_unique<Edge3>();
717  else
718  new_elem_right = std::make_unique<Edge2>();
719 
720  new_elem_right->processor_id() = current_mortar_segment->processor_id();
721  new_elem_right->subdomain_id() = current_mortar_segment->subdomain_id();
722  new_elem_right->set_id(local_id_index++);
723  new_elem_right->set_unique_id(new_elem_right->id());
724  new_elem_right->set_node(0) = new_node;
725  new_elem_right->set_node(1) = current_mortar_segment->node_ptr(1);
726 
727  if (order == SECOND)
728  {
729  // left
730  Point left_interior_point(0);
731  Real left_interior_xi = (xi1 + info->xi1_a) / 2;
732 
733  // This is eta for the current mortar segment that we're splitting
734  Real current_left_interior_eta =
735  (2. * left_interior_xi - info->xi1_a - info->xi1_b) / (info->xi1_b - info->xi1_a);
736 
737  for (MooseIndex(current_mortar_segment->n_nodes()) n = 0;
738  n < current_mortar_segment->n_nodes();
739  ++n)
740  left_interior_point += Moose::fe_lagrange_1D_shape(order, n, current_left_interior_eta) *
741  current_mortar_segment->point(n);
742 
743  const auto new_interior_left_id = _mortar_segment_mesh->max_node_id() + 1;
744  mooseAssert(_mortar_segment_mesh->comm().verify(new_interior_left_id),
745  "new_id must be the same on all processes");
746  Node * const new_interior_node_left = _mortar_segment_mesh->add_point(
747  left_interior_point, new_interior_left_id, new_elem_left->processor_id());
748  new_elem_left->set_node(2) = new_interior_node_left;
749  new_interior_node_left->set_unique_id(new_interior_left_id + node_unique_id_offset);
750 
751  // right
752  Point right_interior_point(0);
753  Real right_interior_xi = (xi1 + info->xi1_b) / 2;
754  // This is eta for the current mortar segment that we're splitting
755  Real current_right_interior_eta =
756  (2. * right_interior_xi - info->xi1_a - info->xi1_b) / (info->xi1_b - info->xi1_a);
757 
758  for (MooseIndex(current_mortar_segment->n_nodes()) n = 0;
759  n < current_mortar_segment->n_nodes();
760  ++n)
761  right_interior_point += Moose::fe_lagrange_1D_shape(order, n, current_right_interior_eta) *
762  current_mortar_segment->point(n);
763 
764  const auto new_interior_id_right = _mortar_segment_mesh->max_node_id() + 1;
765  mooseAssert(_mortar_segment_mesh->comm().verify(new_interior_id_right),
766  "new_id must be the same on all processes");
767  Node * const new_interior_node_right = _mortar_segment_mesh->add_point(
768  right_interior_point, new_interior_id_right, new_elem_right->processor_id());
769  new_elem_right->set_node(2) = new_interior_node_right;
770  new_interior_node_right->set_unique_id(new_interior_id_right + node_unique_id_offset);
771  }
772 
773  // If orientation 2 was valid, swap the left and right primaries.
774  if (orientation2_valid)
775  std::swap(left_primary_elem, right_primary_elem);
776 
777  // Now that we know left_primary_elem and right_primary_elem, we can determine left_xi2 and
778  // right_xi2.
779  if (left_primary_elem)
780  left_xi2 = (primary_node == left_primary_elem->node_ptr(0)) ? -1 : +1;
781  if (right_primary_elem)
782  right_xi2 = (primary_node == right_primary_elem->node_ptr(0)) ? -1 : +1;
783 
784  // Grab the MortarSegmentInfo object associated with this
785  // segment. We can use "at()" here since we want this to fail if
786  // current_mortar_segment is not found... Since we're going to
787  // erase this entry from the map momentarily, we make an actual
788  // copy rather than grabbing a reference.
789  auto msm_it = _msm_elem_to_info.find(current_mortar_segment);
790  if (msm_it == _msm_elem_to_info.end())
791  mooseError("MortarSegmentInfo not found for current_mortar_segment.");
792  MortarSegmentInfo current_msinfo = msm_it->second;
793 
794  // add_left
795  {
796  Elem * msm_new_elem = _mortar_segment_mesh->add_elem(new_elem_left.release());
797 
798  // Create new MortarSegmentInfo objects for new_elem_left
799  MortarSegmentInfo new_msinfo_left;
800 
801  // The new MortarSegmentInfo info objects inherit their "outer"
802  // information from current_msinfo and the rest is determined by
803  // the Node being inserted.
804  new_msinfo_left.xi1_a = current_msinfo.xi1_a;
805  new_msinfo_left.xi2_a = current_msinfo.xi2_a;
806  new_msinfo_left.secondary_elem = secondary_elem;
807  new_msinfo_left.xi1_b = xi1;
808  new_msinfo_left.xi2_b = left_xi2;
809  new_msinfo_left.primary_elem = left_primary_elem;
810 
811  // Add new msinfo objects to the map.
812  _msm_elem_to_info.emplace(msm_new_elem, new_msinfo_left);
813 
814  // We need to insert new_elem_left in
815  // the mortar_segment_set for this secondary_elem.
816  mortar_segment_set.insert(msm_new_elem);
817  }
818 
819  // add_right
820  {
821  Elem * msm_new_elem = _mortar_segment_mesh->add_elem(new_elem_right.release());
822 
823  // Create new MortarSegmentInfo objects for new_elem_right
824  MortarSegmentInfo new_msinfo_right;
825 
826  new_msinfo_right.xi1_b = current_msinfo.xi1_b;
827  new_msinfo_right.xi2_b = current_msinfo.xi2_b;
828  new_msinfo_right.secondary_elem = secondary_elem;
829  new_msinfo_right.xi1_a = xi1;
830  new_msinfo_right.xi2_a = right_xi2;
831  new_msinfo_right.primary_elem = right_primary_elem;
832 
833  _msm_elem_to_info.emplace(msm_new_elem, new_msinfo_right);
834 
835  mortar_segment_set.insert(msm_new_elem);
836  }
837 
838  // Erase the MortarSegmentInfo object for current_mortar_segment from the map.
839  _msm_elem_to_info.erase(msm_it);
840 
841  // current_mortar_segment must be erased from the
842  // mortar_segment_set since it has now been split.
843  mortar_segment_set.erase(current_mortar_segment);
844 
845  // The original mortar segment has been split, so erase it from
846  // the mortar segment mesh.
847  _mortar_segment_mesh->delete_elem(current_mortar_segment);
848  }
849 
850  // Remove all MSM elements without a primary contribution
856  for (auto msm_elem : _mortar_segment_mesh->active_element_ptr_range())
857  {
858  MortarSegmentInfo & msinfo = libmesh_map_find(_msm_elem_to_info, msm_elem);
859  Elem * primary_elem = const_cast<Elem *>(msinfo.primary_elem);
860  if (primary_elem == nullptr || std::abs(msinfo.xi2_a) > 1.0 + TOLERANCE ||
861  std::abs(msinfo.xi2_b) > 1.0 + TOLERANCE)
862  {
863  // Erase from secondary to msms map
864  auto it = _secondary_elems_to_mortar_segments.find(msinfo.secondary_elem->id());
865  mooseAssert(it != _secondary_elems_to_mortar_segments.end(),
866  "We should have found the element");
867  auto & msm_set = it->second;
868  msm_set.erase(msm_elem);
869  // We may be creating nodes with only one element neighbor where before this removal there
870  // were two. But the nodal normal used in computations will reflect the two-neighbor geometry.
871  // For a lower-d secondary mesh corner, that will imply the corner node will have a tilted
872  // normal vector (same for tangents) despite the mortar segment mesh not including its
873  // vertical neighboring element. It is the secondary element neighbors (not mortar segment
874  // mesh neighbors) that determine the nodal normal field.
875  if (msm_set.empty())
877 
878  // Erase msinfo
879  _msm_elem_to_info.erase(msm_elem);
880 
881  // Remove element from mortar segment mesh
882  _mortar_segment_mesh->delete_elem(msm_elem);
883  }
884  else
885  {
888  }
889  }
890 
891  std::unordered_set<Node *> msm_connected_nodes;
892 
893  // Deleting elements may produce isolated nodes.
894  // Loops for identifying and removing such nodes from mortar segment mesh.
895  for (const auto & element : _mortar_segment_mesh->element_ptr_range())
896  for (auto & n : element->node_ref_range())
897  msm_connected_nodes.insert(&n);
898 
899  for (const auto & node : _mortar_segment_mesh->node_ptr_range())
900  if (!msm_connected_nodes.count(node))
901  _mortar_segment_mesh->delete_node(node);
902 
903 #ifdef DEBUG
904  // Verify that all segments without primary contribution have been deleted
905  for (auto msm_elem : _mortar_segment_mesh->active_element_ptr_range())
906  {
907  const MortarSegmentInfo & msinfo = libmesh_map_find(_msm_elem_to_info, msm_elem);
908  mooseAssert(msinfo.primary_elem != nullptr,
909  "All mortar segment elements should have valid "
910  "primary element.");
911  }
912 #endif
913 
914  _mortar_segment_mesh->cache_elem_data();
915 
916  // (Optionally) Write the mortar segment mesh to file for inspection
917  if (_debug)
918  {
919  ExodusII_IO mortar_segment_mesh_writer(*_mortar_segment_mesh);
920 
921  // Default to non-HDF5 output for wider compatibility
922  mortar_segment_mesh_writer.set_hdf5_writing(false);
923 
924  mortar_segment_mesh_writer.write("mortar_segment_mesh.e");
925  }
926 
928 }
929 
930 void
932 {
933  // Add an integer flag to mortar segment mesh to keep track of which subelem
934  // of second order primal elements mortar segments correspond to
935  auto secondary_sub_elem = _mortar_segment_mesh->add_elem_integer("secondary_sub_elem");
936  auto primary_sub_elem = _mortar_segment_mesh->add_elem_integer("primary_sub_elem");
937 
938  dof_id_type local_id_index = 0;
939 
940  // Loop through mortar secondary and primary pairs to create mortar segment mesh between each
941  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
942  {
943  const auto primary_subd_id = pr.first;
944  const auto secondary_subd_id = pr.second;
945 
946  // Build k-d tree for use in Step 1.2 for primary interface coarse screening
947  NanoflannMeshSubdomainAdaptor<3> mesh_adaptor(_mesh, primary_subd_id);
948  subdomain_kd_tree_t kd_tree(
949  3, mesh_adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(/*max leaf=*/10));
950 
951  // Construct the KD tree.
952  kd_tree.buildIndex();
953 
954  // Define expression for getting sub-elements nodes (for sub-dividing secondary elements)
955  auto get_sub_elem_nodes = [](const ElemType type,
956  const unsigned int sub_elem) -> std::vector<unsigned int>
957  {
958  switch (type)
959  {
960  case TRI3:
961  return {{0, 1, 2}};
962  case QUAD4:
963  return {{0, 1, 2, 3}};
964  case TRI6:
965  case TRI7:
966  switch (sub_elem)
967  {
968  case 0:
969  return {{0, 3, 5}};
970  case 1:
971  return {{3, 4, 5}};
972  case 2:
973  return {{3, 1, 4}};
974  case 3:
975  return {{5, 4, 2}};
976  default:
977  mooseError("get_sub_elem_nodes: Invalid sub_elem: ", sub_elem);
978  }
979  case QUAD8:
980  switch (sub_elem)
981  {
982  case 0:
983  return {{0, 4, 7}};
984  case 1:
985  return {{4, 1, 5}};
986  case 2:
987  return {{5, 2, 6}};
988  case 3:
989  return {{7, 6, 3}};
990  case 4:
991  return {{4, 5, 6, 7}};
992  default:
993  mooseError("get_sub_elem_nodes: Invalid sub_elem: ", sub_elem);
994  }
995  case QUAD9:
996  switch (sub_elem)
997  {
998  case 0:
999  return {{0, 4, 8, 7}};
1000  case 1:
1001  return {{4, 1, 5, 8}};
1002  case 2:
1003  return {{8, 5, 2, 6}};
1004  case 3:
1005  return {{7, 8, 6, 3}};
1006  default:
1007  mooseError("get_sub_elem_nodes: Invalid sub_elem: ", sub_elem);
1008  }
1009  default:
1010  mooseError("get_sub_elem_inds: Face element type: ",
1011  libMesh::Utility::enum_to_string<ElemType>(type),
1012  " invalid for 3D mortar");
1013  }
1014  };
1015 
1019  for (MeshBase::const_element_iterator el = _mesh.active_local_elements_begin(),
1020  end_el = _mesh.active_local_elements_end();
1021  el != end_el;
1022  ++el)
1023  {
1024  const Elem * secondary_side_elem = *el;
1025 
1026  const Real secondary_volume = secondary_side_elem->volume();
1027 
1028  // If this Elem is not in the current secondary subdomain, go on to the next one.
1029  if (secondary_side_elem->subdomain_id() != secondary_subd_id)
1030  continue;
1031 
1032  auto [secondary_elem_to_msm_map_it, insertion_happened] =
1033  _secondary_elems_to_mortar_segments.emplace(secondary_side_elem->id(),
1034  std::set<Elem *, CompareDofObjectsByID>{});
1035  libmesh_ignore(insertion_happened);
1036  auto & secondary_to_msm_element_set = secondary_elem_to_msm_map_it->second;
1037 
1038  std::vector<std::unique_ptr<MortarSegmentHelper>> mortar_segment_helper(
1039  secondary_side_elem->n_sub_elem());
1040  const auto nodal_normals = getNodalNormals(*secondary_side_elem);
1041 
1052  for (auto sel : make_range(secondary_side_elem->n_sub_elem()))
1053  {
1054  // Get indices of sub-element nodes in element
1055  auto sub_elem_nodes = get_sub_elem_nodes(secondary_side_elem->type(), sel);
1056 
1057  // Secondary sub-element center, normal, and nodes
1058  Point center;
1059  Point normal;
1060  std::vector<Point> nodes(sub_elem_nodes.size());
1061 
1062  // Loop through sub_element nodes, collect points and compute center and normal
1063  for (auto iv : make_range(sub_elem_nodes.size()))
1064  {
1065  const auto n = sub_elem_nodes[iv];
1066  nodes[iv] = secondary_side_elem->point(n);
1067  center += secondary_side_elem->point(n);
1068  normal += nodal_normals[n];
1069  }
1070  center /= sub_elem_nodes.size();
1071  normal = normal.unit();
1072 
1073  // Build and store linearized sub-elements for later use
1074  mortar_segment_helper[sel] = std::make_unique<MortarSegmentHelper>(nodes, center, normal);
1075  }
1076 
1082  // Search point for performing Nanoflann (k-d tree) searches.
1083  // In each case we use the center point of the original element (not sub-elements for second
1084  // order elements). This is to do search for all sub-elements simultaneously
1085  std::array<Real, 3> query_pt;
1086  Point center_point;
1087  switch (secondary_side_elem->type())
1088  {
1089  case TRI3:
1090  case QUAD4:
1091  center_point = mortar_segment_helper[0]->center();
1092  query_pt = {{center_point(0), center_point(1), center_point(2)}};
1093  break;
1094  case TRI6:
1095  case TRI7:
1096  center_point = mortar_segment_helper[1]->center();
1097  query_pt = {{center_point(0), center_point(1), center_point(2)}};
1098  break;
1099  case QUAD8:
1100  center_point = mortar_segment_helper[4]->center();
1101  query_pt = {{center_point(0), center_point(1), center_point(2)}};
1102  break;
1103  case QUAD9:
1104  center_point = secondary_side_elem->point(8);
1105  query_pt = {{center_point(0), center_point(1), center_point(2)}};
1106  break;
1107  default:
1108  mooseError(
1109  "Face element type: ", secondary_side_elem->type(), "not supported for 3D mortar");
1110  }
1111 
1112  // The number of results we want to get. These results will only be used to find
1113  // a single element with non-trivial overlap, after an element is identified a breadth
1114  // first search is done on neighbors
1115  const std::size_t num_results = 3;
1116 
1117  // Initialize result_set and do the search.
1118  std::vector<size_t> ret_index(num_results);
1119  std::vector<Real> out_dist_sqr(num_results);
1120  nanoflann::KNNResultSet<Real> result_set(num_results);
1121  result_set.init(&ret_index[0], &out_dist_sqr[0]);
1122  kd_tree.findNeighbors(result_set, &query_pt[0], nanoflann::SearchParameters());
1123 
1124  // Initialize list of processed primary elements, we don't want to revisit processed elements
1125  std::set<const Elem *, CompareDofObjectsByID> processed_primary_elems;
1126 
1127  // Initialize candidate set and flag for switching between coarse screening and breadth-first
1128  // search
1129  bool primary_elem_found = false;
1130  std::set<const Elem *, CompareDofObjectsByID> primary_elem_candidates;
1131 
1132  // Loop candidate nodes (returned by Nanoflann) and add all adjoining elems to candidate set
1133  for (auto r : make_range(result_set.size()))
1134  {
1135  // Verify that the squared distance we compute is the same as nanoflann's
1136  mooseAssert(std::abs((_mesh.point(ret_index[r]) - center_point).norm_sq() -
1137  out_dist_sqr[r]) <= TOLERANCE,
1138  "Lower-dimensional element squared distance verification failed.");
1139 
1140  // Get list of elems connected to node
1141  std::vector<const Elem *> & node_elems =
1142  this->_nodes_to_primary_elem_map.at(static_cast<dof_id_type>(ret_index[r]));
1143 
1144  // Uniquely add elems to candidate set
1145  for (auto elem : node_elems)
1146  primary_elem_candidates.insert(elem);
1147  }
1148 
1156  while (!primary_elem_candidates.empty())
1157  {
1158  const Elem * primary_elem_candidate = *primary_elem_candidates.begin();
1159 
1160  // If we've already processed this candidate, we don't need to check it again.
1161  if (processed_primary_elems.count(primary_elem_candidate))
1162  continue;
1163 
1164  // Initialize set of nodes used to construct mortar segment elements
1165  std::vector<Point> nodal_points;
1166 
1167  // Initialize map from mortar segment elements to nodes
1168  std::vector<std::vector<unsigned int>> elem_to_node_map;
1169 
1170  // Initialize list of secondary and primary sub-elements that formed each mortar segment
1171  std::vector<std::pair<unsigned int, unsigned int>> sub_elem_map;
1172 
1177  for (auto p_el : make_range(primary_elem_candidate->n_sub_elem()))
1178  {
1179  // Get nodes of primary sub-elements
1180  auto sub_elem_nodes = get_sub_elem_nodes(primary_elem_candidate->type(), p_el);
1181 
1182  // Get list of primary sub-element vertex nodes
1183  std::vector<Point> primary_sub_elem(sub_elem_nodes.size());
1184  for (auto iv : make_range(sub_elem_nodes.size()))
1185  {
1186  const auto n = sub_elem_nodes[iv];
1187  primary_sub_elem[iv] = primary_elem_candidate->point(n);
1188  }
1189 
1190  // Loop through secondary sub-elements
1191  for (auto s_el : make_range(secondary_side_elem->n_sub_elem()))
1192  {
1193  // Mortar segment helpers were defined for each secondary sub-element, they will:
1194  // 1. Project primary sub-element onto linearized secondary sub-element
1195  // 2. Clip projected primary sub-element against secondary sub-element
1196  // 3. Triangulate clipped polygon to form mortar segments
1197  //
1198  // Mortar segment helpers append a list of mortar segment nodes and connectivities that
1199  // can be directly used to build mortar segments
1200  mortar_segment_helper[s_el]->getMortarSegments(
1201  primary_sub_elem, nodal_points, elem_to_node_map);
1202 
1203  // Keep track of which secondary and primary sub-elements created segment
1204  for (auto i = sub_elem_map.size(); i < elem_to_node_map.size(); ++i)
1205  sub_elem_map.push_back(std::make_pair(s_el, p_el));
1206  }
1207  }
1208 
1209  // Mark primary element as processed and remove from candidate list
1210  processed_primary_elems.insert(primary_elem_candidate);
1211  primary_elem_candidates.erase(primary_elem_candidate);
1212 
1213  // If overlap of polygons was non-trivial (created mortar segment elements)
1214  if (!elem_to_node_map.empty())
1215  {
1216  // If this is the first element with non-trivial overlap, set flag
1217  // Candidates will now be neighbors of elements that had non-trivial overlap
1218  // (i.e. we'll do a breadth first search now)
1219  if (!primary_elem_found)
1220  {
1221  primary_elem_found = true;
1222  primary_elem_candidates.clear();
1223  }
1224 
1225  // Add neighbors to candidate list
1226  for (auto neighbor : primary_elem_candidate->neighbor_ptr_range())
1227  {
1228  // If not valid or not on lower dimensional secondary subdomain, skip
1229  if (neighbor == nullptr || neighbor->subdomain_id() != primary_subd_id)
1230  continue;
1231  // If already processed, skip
1232  if (processed_primary_elems.count(neighbor))
1233  continue;
1234  // Otherwise, add to candidates
1235  primary_elem_candidates.insert(neighbor);
1236  }
1237 
1241  std::vector<Node *> new_nodes;
1242  for (auto pt : nodal_points)
1243  new_nodes.push_back(_mortar_segment_mesh->add_point(
1244  pt, _mortar_segment_mesh->max_node_id(), secondary_side_elem->processor_id()));
1245 
1246  // Loop through triangular elements in map
1247  for (auto el : index_range(elem_to_node_map))
1248  {
1249  // Create new triangular element
1250  std::unique_ptr<Elem> new_elem;
1251  if (elem_to_node_map[el].size() == 3)
1252  new_elem = std::make_unique<Tri3>();
1253  else
1254  mooseError("Active mortar segments only supports TRI elements, 3 nodes expected "
1255  "but: ",
1256  elem_to_node_map[el].size(),
1257  " provided.");
1258 
1259  new_elem->processor_id() = secondary_side_elem->processor_id();
1260  new_elem->subdomain_id() = secondary_side_elem->subdomain_id();
1261  new_elem->set_id(local_id_index++);
1262 
1263  // Attach newly created nodes
1264  for (auto i : index_range(elem_to_node_map[el]))
1265  new_elem->set_node(i) = new_nodes[elem_to_node_map[el][i]];
1266 
1267  // If element is smaller than tolerance, don't add to msm
1268  if (new_elem->volume() / secondary_volume < TOLERANCE)
1269  continue;
1270 
1271  // Add elements to mortar segment mesh
1272  Elem * msm_new_elem = _mortar_segment_mesh->add_elem(new_elem.release());
1273 
1274  msm_new_elem->set_extra_integer(secondary_sub_elem, sub_elem_map[el].first);
1275  msm_new_elem->set_extra_integer(primary_sub_elem, sub_elem_map[el].second);
1276 
1277  // Fill out mortar segment info
1278  MortarSegmentInfo msinfo;
1279  msinfo.secondary_elem = secondary_side_elem;
1280  msinfo.primary_elem = primary_elem_candidate;
1281 
1282  // Associate this MSM elem with the MortarSegmentInfo.
1283  _msm_elem_to_info.emplace(msm_new_elem, msinfo);
1284 
1285  // Add this mortar segment to the secondary elem to mortar segment map
1286  secondary_to_msm_element_set.insert(msm_new_elem);
1287 
1289  // Unlike for 2D, we always have a primary when building the mortar mesh so we don't
1290  // have to check for null
1292  }
1293  }
1294  // End loop through primary element candidates
1295  }
1296 
1297  for (auto sel : make_range(secondary_side_elem->n_sub_elem()))
1298  {
1299  // Check if any segments failed to project
1300  if (mortar_segment_helper[sel]->remainder() == 1.0)
1301  mooseDoOnce(
1302  mooseWarning("Some secondary elements on mortar interface were unable to identify"
1303  " a corresponding primary element; this may be expected depending on"
1304  " problem geometry but may indicate a failure of the element search"
1305  " or projection"));
1306  }
1307 
1308  if (secondary_to_msm_element_set.empty())
1309  _secondary_elems_to_mortar_segments.erase(secondary_elem_to_msm_map_it);
1310  } // End loop through secondary elements
1311  } // End loop through mortar constraint pairs
1312 
1313  _mortar_segment_mesh->cache_elem_data();
1314 
1315  // Output mortar segment mesh
1316  if (_debug)
1317  {
1318  // If element is not triangular, increment subdomain id
1319  // (ExodusII does not support mixed element types in a single subdomain)
1320  for (const auto msm_el : _mortar_segment_mesh->active_local_element_ptr_range())
1321  if (msm_el->type() != TRI3)
1322  msm_el->subdomain_id()++;
1323 
1324  ExodusII_IO mortar_segment_mesh_writer(*_mortar_segment_mesh);
1325 
1326  // Default to non-HDF5 output for wider compatibility
1327  mortar_segment_mesh_writer.set_hdf5_writing(false);
1328 
1329  mortar_segment_mesh_writer.write("mortar_segment_mesh.e");
1330 
1331  // Undo increment
1332  for (const auto msm_el : _mortar_segment_mesh->active_local_element_ptr_range())
1333  if (msm_el->type() != TRI3)
1334  msm_el->subdomain_id()--;
1335  }
1336 
1338 
1339  // Print mortar segment mesh statistics
1340  if (_debug)
1341  {
1342  if (_mesh.n_processors() == 1)
1343  msmStatistics();
1344  else
1345  mooseWarning("Mortar segment mesh statistics intended for debugging purposes in serial only, "
1346  "parallel will only provide statistics for local mortar segment mesh.");
1347  }
1348 }
1349 
1350 void
1352 {
1353  std::unordered_map<processor_id_type, std::vector<std::pair<dof_id_type, dof_id_type>>>
1354  coupling_info;
1355 
1356  // Loop over the msm_elem_to_info object and build a bi-directional
1357  // multimap from secondary elements to the primary Elems which they are
1358  // coupled to and vice-versa. This is used in the
1359  // AugmentSparsityOnInterface functor to determine whether a given
1360  // secondary Elem is coupled across the mortar interface to a primary
1361  // element.
1362  for (const auto & pr : _msm_elem_to_info)
1363  {
1364  const Elem * secondary_elem = pr.second.secondary_elem;
1365  const Elem * primary_elem = pr.second.primary_elem;
1366 
1367  // LowerSecondary
1368  coupling_info[secondary_elem->processor_id()].emplace_back(
1369  secondary_elem->id(), secondary_elem->interior_parent()->id());
1370  if (secondary_elem->processor_id() != _mesh.processor_id())
1371  // We want to keep information for nonlocal lower-dimensional secondary element point
1372  // neighbors for mortar nodal aux kernels
1373  _mortar_interface_coupling[secondary_elem->id()].insert(
1374  secondary_elem->interior_parent()->id());
1375 
1376  // LowerPrimary
1377  coupling_info[secondary_elem->processor_id()].emplace_back(
1378  secondary_elem->id(), primary_elem->interior_parent()->id());
1379  if (secondary_elem->processor_id() != _mesh.processor_id())
1380  // We want to keep information for nonlocal lower-dimensional secondary element point
1381  // neighbors for mortar nodal aux kernels
1382  _mortar_interface_coupling[secondary_elem->id()].insert(
1383  primary_elem->interior_parent()->id());
1384 
1385  // Lower-LowerDimensionalPrimary
1386  coupling_info[secondary_elem->processor_id()].emplace_back(secondary_elem->id(),
1387  primary_elem->id());
1388  if (secondary_elem->processor_id() != _mesh.processor_id())
1389  // We want to keep information for nonlocal lower-dimensional secondary element point
1390  // neighbors for mortar nodal aux kernels
1391  _mortar_interface_coupling[secondary_elem->id()].insert(primary_elem->id());
1392 
1393  // SecondaryLower
1394  coupling_info[secondary_elem->interior_parent()->processor_id()].emplace_back(
1395  secondary_elem->interior_parent()->id(), secondary_elem->id());
1396 
1397  // SecondaryPrimary
1398  coupling_info[secondary_elem->interior_parent()->processor_id()].emplace_back(
1399  secondary_elem->interior_parent()->id(), primary_elem->interior_parent()->id());
1400 
1401  // PrimaryLower
1402  coupling_info[primary_elem->interior_parent()->processor_id()].emplace_back(
1403  primary_elem->interior_parent()->id(), secondary_elem->id());
1404 
1405  // PrimarySecondary
1406  coupling_info[primary_elem->interior_parent()->processor_id()].emplace_back(
1407  primary_elem->interior_parent()->id(), secondary_elem->interior_parent()->id());
1408  }
1409 
1410  // Push the coupling information
1411  auto action_functor =
1412  [this](processor_id_type,
1413  const std::vector<std::pair<dof_id_type, dof_id_type>> & coupling_info)
1414  {
1415  for (auto [i, j] : coupling_info)
1416  _mortar_interface_coupling[i].insert(j);
1417  };
1418  TIMPI::push_parallel_vector_data(_mesh.comm(), coupling_info, action_functor);
1419 }
1420 
1421 void
1423 {
1424  // Print boundary pairs
1425  Moose::out << "Mortar Interface Statistics:" << std::endl;
1426 
1427  // Count number of elements on primary and secondary sides
1428  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1429  {
1430  const auto primary_subd_id = pr.first;
1431  const auto secondary_subd_id = pr.second;
1432 
1433  // Allocate statistics vectors for primary lower, secondary lower, and msm meshes
1434  StatisticsVector<Real> primary; // primary.reserve(mesh.n_elem());
1435  StatisticsVector<Real> secondary; // secondary.reserve(mesh.n_elem());
1436  StatisticsVector<Real> msm; // msm.reserve(mortar_segment_mesh->n_elem());
1437 
1438  for (auto * el : _mesh.active_element_ptr_range())
1439  {
1440  // Add secondary and primary elem volumes to statistics vector
1441  if (el->subdomain_id() == secondary_subd_id)
1442  secondary.push_back(el->volume());
1443  else if (el->subdomain_id() == primary_subd_id)
1444  primary.push_back(el->volume());
1445  }
1446 
1447  // Note: when we allow more than one primary secondary pair will need to make
1448  // separate mortar segment mesh for each
1449  for (auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
1450  {
1451  // Add msm elem volume to statistic vector
1452  msm.push_back(msm_elem->volume());
1453  }
1454 
1455  // Create table
1456  std::vector<std::string> col_names = {"mesh", "n_elems", "max", "min", "median"};
1457  std::vector<std::string> subds = {"secondary_lower", "primary_lower", "mortar_segment"};
1458  std::vector<size_t> n_elems = {secondary.size(), primary.size(), msm.size()};
1459  std::vector<Real> maxs = {secondary.maximum(), primary.maximum(), msm.maximum()};
1460  std::vector<Real> mins = {secondary.minimum(), primary.minimum(), msm.minimum()};
1461  std::vector<Real> medians = {secondary.median(), primary.median(), msm.median()};
1462 
1463  FormattedTable table;
1464  table.clear();
1465  for (auto i : index_range(subds))
1466  {
1467  table.addRow(i);
1468  table.addData<std::string>(col_names[0], subds[i]);
1469  table.addData<size_t>(col_names[1], n_elems[i]);
1470  table.addData<Real>(col_names[2], maxs[i]);
1471  table.addData<Real>(col_names[3], mins[i]);
1472  table.addData<Real>(col_names[4], medians[i]);
1473  }
1474 
1475  Moose::out << "secondary subdomain: " << secondary_subd_id
1476  << " \tprimary subdomain: " << primary_subd_id << std::endl;
1477  table.printTable(Moose::out, subds.size());
1478  }
1479 }
1480 
1481 // The blocks marked with **** are for regressing edge dropping treatment and should be removed
1482 // eventually.
1483 //****
1484 // Compute inactve nodes when the old (incorrect) edge dropping treatemnt is enabled
1485 void
1487 {
1488  // Note that in 3D our trick to check whether an element has edge dropping needs loose tolerances
1489  // since the mortar segments are on the linearized element and comparing the volume of the
1490  // linearized element does not have the same volume as the warped element
1491  const Real tol = (dim() == 3) ? 0.1 : TOLERANCE;
1492 
1493  std::unordered_map<processor_id_type, std::set<dof_id_type>> proc_to_inactive_nodes_set;
1494  const auto my_pid = _mesh.processor_id();
1495 
1496  // List of inactive nodes on local secondary elements
1497  std::unordered_set<dof_id_type> inactive_node_ids;
1498 
1499  std::unordered_map<const Elem *, Real> active_volume{};
1500 
1501  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1502  for (const auto el : _mesh.active_subdomain_elements_ptr_range(pr.second))
1503  active_volume[el] = 0.;
1504 
1505  // Compute fraction of elements with corresponding primary elements
1506  for (const auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
1507  {
1508  const MortarSegmentInfo & msinfo = _msm_elem_to_info.at(msm_elem);
1509  const Elem * secondary_elem = msinfo.secondary_elem;
1510 
1511  active_volume[secondary_elem] += msm_elem->volume();
1512  }
1513 
1514  // Mark all inactive local nodes
1515  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1516  // Loop through all elements on my processor
1517  for (const auto el : _mesh.active_local_subdomain_elements_ptr_range(pr.second))
1518  // If elem fully or partially dropped
1519  if (std::abs(active_volume[el] / el->volume() - 1.0) > tol)
1520  {
1521  // Add all nodes to list of inactive
1522  for (auto n : make_range(el->n_nodes()))
1523  inactive_node_ids.insert(el->node_id(n));
1524  }
1525 
1526  // Assemble list of procs that nodes contribute to
1527  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1528  {
1529  const auto secondary_subd_id = pr.second;
1530 
1531  // Loop through all elements not on my processor
1532  for (const auto el : _mesh.active_subdomain_elements_ptr_range(secondary_subd_id))
1533  {
1534  // Get processor_id
1535  const auto pid = el->processor_id();
1536 
1537  // If element is in my subdomain, skip
1538  if (pid == my_pid)
1539  continue;
1540 
1541  // If element on proc pid shares any of my inactive nodes, mark to send
1542  for (const auto n : make_range(el->n_nodes()))
1543  {
1544  const auto node_id = el->node_id(n);
1545  if (inactive_node_ids.find(node_id) != inactive_node_ids.end())
1546  proc_to_inactive_nodes_set[pid].insert(node_id);
1547  }
1548  }
1549  }
1550 
1551  // Send list of inactive nodes
1552  {
1553  // Pack set into vector for sending (push_parallel_vector_data doesn't like sets)
1554  std::unordered_map<processor_id_type, std::vector<dof_id_type>> proc_to_inactive_nodes_vector;
1555  for (const auto & proc_set : proc_to_inactive_nodes_set)
1556  proc_to_inactive_nodes_vector[proc_set.first].insert(
1557  proc_to_inactive_nodes_vector[proc_set.first].end(),
1558  proc_set.second.begin(),
1559  proc_set.second.end());
1560 
1561  // First push data
1562  auto action_functor = [this, &inactive_node_ids](const processor_id_type pid,
1563  const std::vector<dof_id_type> & sent_data)
1564  {
1565  if (pid == _mesh.processor_id())
1566  mooseError("Should not be communicating with self.");
1567  for (const auto pr : sent_data)
1568  inactive_node_ids.insert(pr);
1569  };
1570  TIMPI::push_parallel_vector_data(_mesh.comm(), proc_to_inactive_nodes_vector, action_functor);
1571  }
1572  _inactive_local_lm_nodes.clear();
1573  for (const auto node_id : inactive_node_ids)
1574  _inactive_local_lm_nodes.insert(_mesh.node_ptr(node_id));
1575 }
1576 
1577 void
1579 {
1581  {
1583  return;
1584  }
1585 
1586  std::unordered_map<processor_id_type, std::set<dof_id_type>> proc_to_active_nodes_set;
1587  const auto my_pid = _mesh.processor_id();
1588 
1589  // List of active nodes on local secondary elements
1590  std::unordered_set<dof_id_type> active_local_nodes;
1591 
1592  // Mark all active local nodes
1593  for (const auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
1594  {
1595  const MortarSegmentInfo & msinfo = _msm_elem_to_info.at(msm_elem);
1596  const Elem * secondary_elem = msinfo.secondary_elem;
1597 
1598  for (auto n : make_range(secondary_elem->n_nodes()))
1599  active_local_nodes.insert(secondary_elem->node_id(n));
1600  }
1601 
1602  // Assemble list of procs that nodes contribute to
1603  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1604  {
1605  const auto secondary_subd_id = pr.second;
1606 
1607  // Loop through all elements not on my processor
1608  for (const auto el : _mesh.active_subdomain_elements_ptr_range(secondary_subd_id))
1609  {
1610  // Get processor_id
1611  const auto pid = el->processor_id();
1612 
1613  // If element is in my subdomain, skip
1614  if (pid == my_pid)
1615  continue;
1616 
1617  // If element on proc pid shares any of my active nodes, mark to send
1618  for (const auto n : make_range(el->n_nodes()))
1619  {
1620  const auto node_id = el->node_id(n);
1621  if (active_local_nodes.find(node_id) != active_local_nodes.end())
1622  proc_to_active_nodes_set[pid].insert(node_id);
1623  }
1624  }
1625  }
1626 
1627  // Send list of active nodes
1628  {
1629  // Pack set into vector for sending (push_parallel_vector_data doesn't like sets)
1630  std::unordered_map<processor_id_type, std::vector<dof_id_type>> proc_to_active_nodes_vector;
1631  for (const auto & proc_set : proc_to_active_nodes_set)
1632  {
1633  proc_to_active_nodes_vector[proc_set.first].reserve(proc_to_active_nodes_set.size());
1634  for (const auto node_id : proc_set.second)
1635  proc_to_active_nodes_vector[proc_set.first].push_back(node_id);
1636  }
1637 
1638  // First push data
1639  auto action_functor = [this, &active_local_nodes](const processor_id_type pid,
1640  const std::vector<dof_id_type> & sent_data)
1641  {
1642  if (pid == _mesh.processor_id())
1643  mooseError("Should not be communicating with self.");
1644  active_local_nodes.insert(sent_data.begin(), sent_data.end());
1645  };
1646  TIMPI::push_parallel_vector_data(_mesh.comm(), proc_to_active_nodes_vector, action_functor);
1647  }
1648 
1649  // Every proc has correct list of active local nodes, now take complement (list of inactive nodes)
1650  // and store to use later to zero LM DoFs on inactive nodes
1651  _inactive_local_lm_nodes.clear();
1652  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1653  for (const auto el : _mesh.active_local_subdomain_elements_ptr_range(
1654  /*secondary_subd_id*/ pr.second))
1655  for (const auto n : make_range(el->n_nodes()))
1656  if (active_local_nodes.find(el->node_id(n)) == active_local_nodes.end())
1657  _inactive_local_lm_nodes.insert(el->node_ptr(n));
1658 }
1659 
1660 // Note: could be combined with previous routine, keeping separate for clarity (for now)
1661 void
1663 {
1664  // Mark all active secondary elements
1665  std::unordered_set<const Elem *> active_local_elems;
1666 
1667  //****
1668  // Note that in 3D our trick to check whether an element has edge dropping needs loose tolerances
1669  // since the mortar segments are on the linearized element and comparing the volume of the
1670  // linearized element does not have the same volume as the warped element
1671  const Real tol = (dim() == 3) ? 0.1 : TOLERANCE;
1672 
1673  std::unordered_map<const Elem *, Real> active_volume;
1674 
1675  // Compute fraction of elements with corresponding primary elements
1677  for (const auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
1678  {
1679  const MortarSegmentInfo & msinfo = _msm_elem_to_info.at(msm_elem);
1680  const Elem * secondary_elem = msinfo.secondary_elem;
1681 
1682  active_volume[secondary_elem] += msm_elem->volume();
1683  }
1684  //****
1685 
1686  for (const auto msm_elem : _mortar_segment_mesh->active_local_element_ptr_range())
1687  {
1688  const MortarSegmentInfo & msinfo = _msm_elem_to_info.at(msm_elem);
1689  const Elem * secondary_elem = msinfo.secondary_elem;
1690 
1691  //****
1693  if (std::abs(active_volume[secondary_elem] / secondary_elem->volume() - 1.0) > tol)
1694  continue;
1695  //****
1696 
1697  active_local_elems.insert(secondary_elem);
1698  }
1699 
1700  // Take complement of active elements in active local subdomain to get inactive local elements
1701  _inactive_local_lm_elems.clear();
1702  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1703  for (const auto el : _mesh.active_local_subdomain_elements_ptr_range(
1704  /*secondary_subd_id*/ pr.second))
1705  if (active_local_elems.find(el) == active_local_elems.end())
1706  _inactive_local_lm_elems.insert(el);
1707 }
1708 
1709 void
1711 {
1712  // The dimension according to Mesh::mesh_dimension().
1713  const auto dim = _mesh.mesh_dimension();
1714 
1715  // A nodal lower-dimensional nodal quadrature rule to be used on faces.
1716  QNodal qface(dim - 1);
1717 
1718  // A map from the node id to the attached elemental normals/weights evaluated at the node. Th
1719  // length of the vector will correspond to the number of elements attached to the node. If it is a
1720  // vertex node, for a 1D mortar mesh, the vector length will be two. If it is an interior node,
1721  // the vector will be length 1. The first member of the pair is that element's normal at the node.
1722  // The second member is that element's JxW at the node
1723  std::map<dof_id_type, std::vector<std::pair<Point, Real>>> node_to_normals_map;
1724 
1726  Real sign = _periodic ? -1 : 1;
1727 
1728  // First loop over lower-dimensional secondary side elements and compute/save the outward normal
1729  // for each one. We loop over all active elements currently, but this procedure could be
1730  // parallelized as well.
1731  for (MeshBase::const_element_iterator el = _mesh.active_elements_begin(),
1732  end_el = _mesh.active_elements_end();
1733  el != end_el;
1734  ++el)
1735  {
1736  const Elem * secondary_elem = *el;
1737 
1738  // If this is not one of the lower-dimensional secondary side elements, go on to the next one.
1739  if (!_secondary_boundary_subdomain_ids.count(secondary_elem->subdomain_id()))
1740  continue;
1741 
1742  // We will create an FE object and attach the nodal quadrature rule such that we can get out the
1743  // normals at the element nodes
1744  FEType nnx_fe_type(secondary_elem->default_order(), LAGRANGE);
1745  std::unique_ptr<FEBase> nnx_fe_face(FEBase::build(dim, nnx_fe_type));
1746  nnx_fe_face->attach_quadrature_rule(&qface);
1747  const std::vector<Point> & face_normals = nnx_fe_face->get_normals();
1748 
1749  const auto & JxW = nnx_fe_face->get_JxW();
1750 
1751  // Which side of the parent are we? We need to know this to know
1752  // which side to reinit.
1753  const Elem * interior_parent = secondary_elem->interior_parent();
1754  mooseAssert(interior_parent,
1755  "No interior parent exists for element "
1756  << secondary_elem->id()
1757  << ". There may be a problem with your sideset set-up.");
1758 
1759  // Map to get lower dimensional element from interior parent on secondary surface
1760  // This map can be used to provide a handle to methods in this class that need to
1761  // operate on lower dimensional elements.
1762  _secondary_element_to_secondary_lowerd_element.emplace(interior_parent->id(), secondary_elem);
1763 
1764  // Look up which side of the interior parent secondary_elem is.
1765  auto s = interior_parent->which_side_am_i(secondary_elem);
1766 
1767  // Reinit the face FE object on side s.
1768  nnx_fe_face->reinit(interior_parent, s);
1769 
1770  for (MooseIndex(secondary_elem->n_nodes()) n = 0; n < secondary_elem->n_nodes(); ++n)
1771  {
1772  auto & normals_and_weights_vec = node_to_normals_map[secondary_elem->node_id(n)];
1773  normals_and_weights_vec.push_back(std::make_pair(sign * face_normals[n], JxW[n]));
1774  }
1775  }
1776 
1777  // Note that contrary to the Bin Yang dissertation, we are not weighting by the face element
1778  // lengths/volumes. It's not clear to me that this type of weighting is a good algorithm for cases
1779  // where the face can be curved
1780  for (const auto & pr : node_to_normals_map)
1781  {
1782  // Compute normal vector
1783  const auto & node_id = pr.first;
1784  const auto & normals_and_weights_vec = pr.second;
1785 
1786  Point nodal_normal;
1787  for (const auto & norm_and_weight : normals_and_weights_vec)
1788  nodal_normal += norm_and_weight.first * norm_and_weight.second;
1789  nodal_normal = nodal_normal.unit();
1790 
1791  _secondary_node_to_nodal_normal[_mesh.node_ptr(node_id)] = nodal_normal;
1792 
1793  Point nodal_tangent_one;
1794  Point nodal_tangent_two;
1795  householderOrthogolization(nodal_normal, nodal_tangent_one, nodal_tangent_two);
1796 
1797  _secondary_node_to_hh_nodal_tangents[_mesh.node_ptr(node_id)][0] = nodal_tangent_one;
1798  _secondary_node_to_hh_nodal_tangents[_mesh.node_ptr(node_id)][1] = nodal_tangent_two;
1799  }
1800 }
1801 
1802 void
1804  Point & nodal_tangent_one,
1805  Point & nodal_tangent_two) const
1806 {
1807  mooseAssert(MooseUtils::absoluteFuzzyEqual(nodal_normal.norm(), 1),
1808  "The input nodal normal should have unity norm");
1809 
1810  const Real nx = nodal_normal(0);
1811  const Real ny = nodal_normal(1);
1812  const Real nz = nodal_normal(2);
1813 
1814  // See Lopes DS, Silva MT, Ambrosio JA. Tangent vectors to a 3-D surface normal: A geometric tool
1815  // to find orthogonal vectors based on the Householder transformation. Computer-Aided Design. 2013
1816  // Mar 1;45(3):683-94. We choose one definition of h_vector and deal with special case.
1817  const Point h_vector(nx + 1.0, ny, nz);
1818 
1819  // Avoid singularity of the equations at the end of routine by providing the solution to
1820  // (nx,ny,nz)=(-1,0,0) Normal/tangent fields can be visualized by outputting nodal geometry mesh
1821  // on a spherical problem.
1822  if (std::abs(h_vector(0)) < TOLERANCE)
1823  {
1824  nodal_tangent_one(0) = 0;
1825  nodal_tangent_one(1) = 1;
1826  nodal_tangent_one(2) = 0;
1827 
1828  nodal_tangent_two(0) = 0;
1829  nodal_tangent_two(1) = 0;
1830  nodal_tangent_two(2) = -1;
1831 
1832  return;
1833  }
1834 
1835  const Real h = h_vector.norm();
1836 
1837  nodal_tangent_one(0) = -2.0 * h_vector(0) * h_vector(1) / (h * h);
1838  nodal_tangent_one(1) = 1.0 - 2.0 * h_vector(1) * h_vector(1) / (h * h);
1839  nodal_tangent_one(2) = -2.0 * h_vector(1) * h_vector(2) / (h * h);
1840 
1841  nodal_tangent_two(0) = -2.0 * h_vector(0) * h_vector(2) / (h * h);
1842  nodal_tangent_two(1) = -2.0 * h_vector(1) * h_vector(2) / (h * h);
1843  nodal_tangent_two(2) = 1.0 - 2.0 * h_vector(2) * h_vector(2) / (h * h);
1844 }
1845 
1846 // Project secondary nodes onto their corresponding primary elements for each primary/secondary
1847 // pair.
1848 void
1850 {
1851  // For each primary/secondary boundary id pair, call the
1852  // project_secondary_nodes_single_pair() helper function.
1853  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
1854  projectSecondaryNodesSinglePair(pr.first, pr.second);
1855 }
1856 
1857 void
1859  SubdomainID lower_dimensional_primary_subdomain_id,
1860  SubdomainID lower_dimensional_secondary_subdomain_id)
1861 {
1862  // Build the "subdomain" adaptor based KD Tree.
1863  NanoflannMeshSubdomainAdaptor<3> mesh_adaptor(_mesh, lower_dimensional_primary_subdomain_id);
1864  subdomain_kd_tree_t kd_tree(
1865  3, mesh_adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(/*max leaf=*/10));
1866 
1867  // Construct the KD tree.
1868  kd_tree.buildIndex();
1869 
1870  for (MeshBase::const_element_iterator el = _mesh.active_elements_begin(),
1871  end_el = _mesh.active_elements_end();
1872  el != end_el;
1873  ++el)
1874  {
1875  const Elem * secondary_side_elem = *el;
1876 
1877  // If this Elem is not in the current secondary subdomain, go on to the next one.
1878  if (secondary_side_elem->subdomain_id() != lower_dimensional_secondary_subdomain_id)
1879  continue;
1880 
1881  // For each node on the lower-dimensional element, find the nearest
1882  // node on the primary side using the KDTree, then
1883  // search in nearby elements for where it projects
1884  // along the nodal normal direction.
1885  for (MooseIndex(secondary_side_elem->n_vertices()) n = 0; n < secondary_side_elem->n_vertices();
1886  ++n)
1887  {
1888  const Node * secondary_node = secondary_side_elem->node_ptr(n);
1889 
1890  // Get the nodal neighbors for secondary_node, so we can check whether we've
1891  // already successfully projected it.
1892  const std::vector<const Elem *> & secondary_node_neighbors =
1893  this->_nodes_to_secondary_elem_map.at(secondary_node->id());
1894 
1895  // Check whether we've already mapped this secondary node
1896  // successfully for all of its nodal neighbors.
1897  bool is_mapped = true;
1898  for (MooseIndex(secondary_node_neighbors) snn = 0; snn < secondary_node_neighbors.size();
1899  ++snn)
1900  {
1901  auto secondary_key = std::make_pair(secondary_node, secondary_node_neighbors[snn]);
1902  if (!_secondary_node_and_elem_to_xi2_primary_elem.count(secondary_key))
1903  {
1904  is_mapped = false;
1905  break;
1906  }
1907  }
1908 
1909  // Go to the next node if this one has already been mapped.
1910  if (is_mapped)
1911  continue;
1912 
1913  // Look up the new nodal normal value in the local storage, error if not found.
1914  Point nodal_normal = _secondary_node_to_nodal_normal.at(secondary_node);
1915 
1916  // Data structure for performing Nanoflann searches.
1917  std::array<Real, 3> query_pt = {
1918  {(*secondary_node)(0), (*secondary_node)(1), (*secondary_node)(2)}};
1919 
1920  // The number of results we want to get. We'll look for a
1921  // "few" nearest nodes, hopefully that is enough to let us
1922  // figure out which lower-dimensional Elem on the primary
1923  // side we are across from.
1924  const std::size_t num_results = 3;
1925 
1926  // Initialize result_set and do the search.
1927  std::vector<size_t> ret_index(num_results);
1928  std::vector<Real> out_dist_sqr(num_results);
1929  nanoflann::KNNResultSet<Real> result_set(num_results);
1930  result_set.init(&ret_index[0], &out_dist_sqr[0]);
1931  kd_tree.findNeighbors(result_set, &query_pt[0], nanoflann::SearchParameters());
1932 
1933  // If this flag gets set in the loop below, we can break out of the outer r-loop as well.
1934  bool projection_succeeded = false;
1935 
1936  // Once we've rejected a candidate for a given secondary_node,
1937  // there's no reason to check it again.
1938  std::set<const Elem *> rejected_primary_elem_candidates;
1939 
1940  // Loop over the closest nodes, check whether
1941  // the secondary node successfully projects into
1942  // either of the closest neighbors, stop when
1943  // the projection succeeds.
1944  for (MooseIndex(result_set) r = 0; r < result_set.size(); ++r)
1945  {
1946  // Verify that the squared distance we compute is the same as nanoflann'sFss
1947  mooseAssert(std::abs((_mesh.point(ret_index[r]) - *secondary_node).norm_sq() -
1948  out_dist_sqr[r]) <= TOLERANCE,
1949  "Lower-dimensional element squared distance verification failed.");
1950 
1951  // Get a reference to the vector of lower dimensional elements from the
1952  // nodes_to_primary_elem_map.
1953  std::vector<const Elem *> & primary_elem_candidates =
1954  this->_nodes_to_primary_elem_map.at(static_cast<dof_id_type>(ret_index[r]));
1955 
1956  // Search the Elems connected to this node on the primary mesh side.
1957  for (MooseIndex(primary_elem_candidates) e = 0; e < primary_elem_candidates.size(); ++e)
1958  {
1959  const Elem * primary_elem_candidate = primary_elem_candidates[e];
1960 
1961  // If we've already rejected this candidate, we don't need to check it again.
1962  if (rejected_primary_elem_candidates.count(primary_elem_candidate))
1963  continue;
1964 
1965  // Now generically solve for xi2
1966  auto && order = primary_elem_candidate->default_order();
1967  DualNumber<Real> xi2_dn{0, 1};
1968  unsigned int current_iterate = 0, max_iterates = 10;
1969 
1970  // Newton loop
1971  do
1972  {
1974  for (MooseIndex(primary_elem_candidate->n_nodes()) n = 0;
1975  n < primary_elem_candidate->n_nodes();
1976  ++n)
1977  x2 +=
1978  Moose::fe_lagrange_1D_shape(order, n, xi2_dn) * primary_elem_candidate->point(n);
1979  const auto u = x2 - (*secondary_node);
1980  const auto F = u(0) * nodal_normal(1) - u(1) * nodal_normal(0);
1981 
1982  if (std::abs(F) < _newton_tolerance)
1983  break;
1984 
1985  if (F.derivatives())
1986  {
1987  Real dxi2 = -F.value() / F.derivatives();
1988 
1989  xi2_dn += dxi2;
1990  }
1991  else
1992  // It's possible that the secondary surface nodal normal is completely orthogonal to
1993  // the primary surface normal, in which case the derivative is 0. We know in this case
1994  // that the projection should be a failure
1995  current_iterate = max_iterates;
1996  } while (++current_iterate < max_iterates);
1997 
1998  Real xi2 = xi2_dn.value();
1999 
2000  // Check whether the projection worked. The last condition checks for obliqueness of the
2001  // projection
2002  if ((current_iterate < max_iterates) && (std::abs(xi2) <= 1. + _xi_tolerance) &&
2003  (std::abs(
2004  (primary_elem_candidate->point(0) - primary_elem_candidate->point(1)).unit() *
2005  nodal_normal) < std::cos(_minimum_projection_angle * libMesh::pi / 180)))
2006  {
2007  // If xi2 == +1 or -1 then this secondary node mapped directly to a node on the primary
2008  // surface. This isn't as unlikely as you might think, it will happen if the meshes
2009  // on the interface start off being perfectly aligned. In this situation, we need to
2010  // associate the secondary node with two different elements (and two corresponding
2011  // xi^(2) values.
2012 
2013  // We are projecting on one side first and the other side second. If we make the
2014  // tolerance bigger and remove the (5) factor we are going to continue to miss the
2015  // second projection and fall into the exception message in
2016  // projectPrimaryNodesSinglePair. What makes this modification to not fall in the
2017  // exception is that we are projecting on one side more xi than in the other. There
2018  // should be a better way of doing this by using actual distances and not parametric
2019  // coordinates. But I believe making the tolerance uniformly larger or smaller won't do
2020  // the trick here.
2021  if (std::abs(std::abs(xi2) - 1.) < _xi_tolerance * 5.0)
2022  {
2023  const Node * primary_node = (xi2 < 0) ? primary_elem_candidate->node_ptr(0)
2024  : primary_elem_candidate->node_ptr(1);
2025 
2026  const std::vector<const Elem *> & primary_node_neighbors =
2027  _nodes_to_primary_elem_map.at(primary_node->id());
2028 
2029  std::vector<bool> primary_elems_mapped(primary_node_neighbors.size(), false);
2030 
2031  // Add entries to secondary_node_and_elem_to_xi2_primary_elem container.
2032  //
2033  // First, determine "on left" vs. "on right" orientation of the nodal neighbors.
2034  // There can be a max of 2 nodal neighbors, and we want to make sure that the
2035  // secondary nodal neighbor on the "left" is associated with the primary nodal
2036  // neighbor on the "left" and similarly for the "right".
2037  std::vector<Real> secondary_node_neighbor_cps(2), primary_node_neighbor_cps(2);
2038 
2039  // Figure out which secondary side neighbor is on the "left" and which is on the
2040  // "right".
2041  for (MooseIndex(secondary_node_neighbors) nn = 0;
2042  nn < secondary_node_neighbors.size();
2043  ++nn)
2044  {
2045  const Elem * secondary_neigh = secondary_node_neighbors[nn];
2046  Point opposite = (secondary_neigh->node_ptr(0) == secondary_node)
2047  ? secondary_neigh->point(1)
2048  : secondary_neigh->point(0);
2049  Point cp = nodal_normal.cross(opposite - *secondary_node);
2050  secondary_node_neighbor_cps[nn] = cp(2);
2051  }
2052 
2053  // Figure out which primary side neighbor is on the "left" and which is on the
2054  // "right".
2055  for (MooseIndex(primary_node_neighbors) nn = 0; nn < primary_node_neighbors.size();
2056  ++nn)
2057  {
2058  const Elem * primary_neigh = primary_node_neighbors[nn];
2059  Point opposite = (primary_neigh->node_ptr(0) == primary_node)
2060  ? primary_neigh->point(1)
2061  : primary_neigh->point(0);
2062  Point cp = nodal_normal.cross(opposite - *secondary_node);
2063  primary_node_neighbor_cps[nn] = cp(2);
2064  }
2065 
2066  // Associate secondary/primary elems on matching sides.
2067  bool found_match = false;
2068  for (MooseIndex(secondary_node_neighbors) snn = 0;
2069  snn < secondary_node_neighbors.size();
2070  ++snn)
2071  for (MooseIndex(primary_node_neighbors) mnn = 0;
2072  mnn < primary_node_neighbors.size();
2073  ++mnn)
2074  if (secondary_node_neighbor_cps[snn] * primary_node_neighbor_cps[mnn] > 0)
2075  {
2076  found_match = true;
2077  primary_elems_mapped[mnn] = true;
2078 
2079  // Figure out xi^(2) value by looking at which node primary_node is
2080  // of the current primary node neighbor.
2081  Real xi2 = (primary_node == primary_node_neighbors[mnn]->node_ptr(0)) ? -1 : +1;
2082  auto secondary_key =
2083  std::make_pair(secondary_node, secondary_node_neighbors[snn]);
2084  auto primary_val = std::make_pair(xi2, primary_node_neighbors[mnn]);
2085  _secondary_node_and_elem_to_xi2_primary_elem.emplace(secondary_key,
2086  primary_val);
2087 
2088  // Also map in the other direction.
2089  Real xi1 =
2090  (secondary_node == secondary_node_neighbors[snn]->node_ptr(0)) ? -1 : +1;
2091 
2092  auto primary_key = std::make_tuple(
2093  primary_node->id(), primary_node, primary_node_neighbors[mnn]);
2094  auto secondary_val = std::make_pair(xi1, secondary_node_neighbors[snn]);
2096  secondary_val);
2097  }
2098 
2099  if (!found_match)
2100  {
2101  // There could be coincident nodes and this might be a bad primary candidate (see
2102  // issue #21680). Instead of giving up, let's try continuing
2103  rejected_primary_elem_candidates.insert(primary_elem_candidate);
2104  continue;
2105  }
2106 
2107  // We need to handle the case where we've exactly projected a secondary node onto a
2108  // primary node, but our secondary node is at one of the secondary face endpoints and
2109  // our primary node is not.
2110  if (secondary_node_neighbors.size() == 1 && primary_node_neighbors.size() == 2)
2111  for (auto it = primary_elems_mapped.begin(); it != primary_elems_mapped.end(); ++it)
2112  if (*it == false)
2113  {
2114  auto index = std::distance(primary_elems_mapped.begin(), it);
2116  std::make_tuple(
2117  primary_node->id(), primary_node, primary_node_neighbors[index]),
2118  std::make_pair(1, nullptr));
2119  }
2120  }
2121  else // Point falls somewhere in the middle of the Elem.
2122  {
2123  // Add two entries to secondary_node_and_elem_to_xi2_primary_elem.
2124  for (MooseIndex(secondary_node_neighbors) nn = 0;
2125  nn < secondary_node_neighbors.size();
2126  ++nn)
2127  {
2128  const Elem * neigh = secondary_node_neighbors[nn];
2129  for (MooseIndex(neigh->n_vertices()) nid = 0; nid < neigh->n_vertices(); ++nid)
2130  {
2131  const Node * neigh_node = neigh->node_ptr(nid);
2132  if (secondary_node == neigh_node)
2133  {
2134  auto key = std::make_pair(neigh_node, neigh);
2135  auto val = std::make_pair(xi2, primary_elem_candidate);
2137  }
2138  }
2139  }
2140  }
2141 
2142  projection_succeeded = true;
2143  break; // out of e-loop
2144  }
2145  else
2146  // The current secondary_node is not in this Elem, so keep track of the rejects.
2147  rejected_primary_elem_candidates.insert(primary_elem_candidate);
2148  }
2149 
2150  if (projection_succeeded)
2151  break; // out of r-loop
2152  } // r-loop
2153 
2154  if (!projection_succeeded && _debug)
2155  _console << "Failed to find primary Elem into which secondary node "
2156  << static_cast<const Point &>(*secondary_node) << " was projected." << std::endl
2157  << std::endl;
2158  } // loop over side nodes
2159  } // end loop over lower-dimensional elements
2160 }
2161 
2162 // Inverse map primary nodes onto their corresponding secondary elements for each primary/secondary
2163 // pair.
2164 void
2166 {
2167  // For each primary/secondary boundary id pair, call the
2168  // project_primary_nodes_single_pair() helper function.
2169  for (const auto & pr : _primary_secondary_subdomain_id_pairs)
2170  projectPrimaryNodesSinglePair(pr.first, pr.second);
2171 }
2172 
2173 void
2175  SubdomainID lower_dimensional_primary_subdomain_id,
2176  SubdomainID lower_dimensional_secondary_subdomain_id)
2177 {
2178  // Build a Nanoflann object on the lower-dimensional secondary elements of the Mesh.
2179  NanoflannMeshSubdomainAdaptor<3> mesh_adaptor(_mesh, lower_dimensional_secondary_subdomain_id);
2180  subdomain_kd_tree_t kd_tree(
2181  3, mesh_adaptor, nanoflann::KDTreeSingleIndexAdaptorParams(/*max leaf=*/10));
2182 
2183  // Construct the KD tree for lower-dimensional elements in the volume mesh.
2184  kd_tree.buildIndex();
2185 
2186  std::unordered_set<dof_id_type> primary_nodes_visited;
2187 
2188  for (const auto & primary_side_elem : _mesh.active_element_ptr_range())
2189  {
2190  // If this is not one of the lower-dimensional primary side elements, go on to the next one.
2191  if (primary_side_elem->subdomain_id() != lower_dimensional_primary_subdomain_id)
2192  continue;
2193 
2194  // For each node on this side, find the nearest node on the secondary side using the KDTree,
2195  // then search in nearby elements for where it projects along the nodal normal direction.
2196  for (MooseIndex(primary_side_elem->n_vertices()) n = 0; n < primary_side_elem->n_vertices();
2197  ++n)
2198  {
2199  // Get a pointer to this node.
2200  const Node * primary_node = primary_side_elem->node_ptr(n);
2201 
2202  // Get the nodal neighbors connected to this primary node.
2203  const std::vector<const Elem *> & primary_node_neighbors =
2204  _nodes_to_primary_elem_map.at(primary_node->id());
2205 
2206  // Check whether we have already successfully inverse mapped this primary node (whether during
2207  // secondary node projection or now during primary node projection) or we have already failed
2208  // to inverse map this primary node (now during primary node projection), and then skip if
2209  // either of those things is true
2210  auto primary_key =
2211  std::make_tuple(primary_node->id(), primary_node, primary_node_neighbors[0]);
2212  if (!primary_nodes_visited.insert(primary_node->id()).second ||
2214  continue;
2215 
2216  // Data structure for performing Nanoflann searches.
2217  Real query_pt[3] = {(*primary_node)(0), (*primary_node)(1), (*primary_node)(2)};
2218 
2219  // The number of results we want to get. We'll look for a
2220  // "few" nearest nodes, hopefully that is enough to let us
2221  // figure out which lower-dimensional Elem on the secondary side
2222  // we are across from.
2223  const size_t num_results = 3;
2224 
2225  // Initialize result_set and do the search.
2226  std::vector<size_t> ret_index(num_results);
2227  std::vector<Real> out_dist_sqr(num_results);
2228  nanoflann::KNNResultSet<Real> result_set(num_results);
2229  result_set.init(&ret_index[0], &out_dist_sqr[0]);
2230  kd_tree.findNeighbors(result_set, &query_pt[0], nanoflann::SearchParameters());
2231 
2232  // If this flag gets set in the loop below, we can break out of the outer r-loop as well.
2233  bool projection_succeeded = false;
2234 
2235  // Once we've rejected a candidate for a given
2236  // primary_node, there's no reason to check it
2237  // again.
2238  std::set<const Elem *> rejected_secondary_elem_candidates;
2239 
2240  // Loop over the closest nodes, check whether the secondary node successfully projects into
2241  // either of the closest neighbors, stop when the projection succeeds.
2242  for (MooseIndex(result_set) r = 0; r < result_set.size(); ++r)
2243  {
2244  // Verify that the squared distance we compute is the same as nanoflann's
2245  mooseAssert(std::abs((_mesh.point(ret_index[r]) - *primary_node).norm_sq() -
2246  out_dist_sqr[r]) <= TOLERANCE,
2247  "Lower-dimensional element squared distance verification failed.");
2248 
2249  // Get a reference to the vector of lower dimensional elements from the
2250  // nodes_to_secondary_elem_map.
2251  const std::vector<const Elem *> & secondary_elem_candidates =
2252  _nodes_to_secondary_elem_map.at(static_cast<dof_id_type>(ret_index[r]));
2253 
2254  // Print the Elems connected to this node on the secondary mesh side.
2255  for (MooseIndex(secondary_elem_candidates) e = 0; e < secondary_elem_candidates.size(); ++e)
2256  {
2257  const Elem * secondary_elem_candidate = secondary_elem_candidates[e];
2258 
2259  // If we've already rejected this candidate, we don't need to check it again.
2260  if (rejected_secondary_elem_candidates.count(secondary_elem_candidate))
2261  continue;
2262 
2263  std::vector<Point> nodal_normals(secondary_elem_candidate->n_nodes());
2264  for (const auto n : make_range(secondary_elem_candidate->n_nodes()))
2265  nodal_normals[n] =
2266  _secondary_node_to_nodal_normal.at(secondary_elem_candidate->node_ptr(n));
2267 
2268  // Use equation 2.4.6 from Bin Yang's dissertation to try and solve for
2269  // the position on the secondary element where this primary came from. This
2270  // requires a Newton iteration in general.
2271  DualNumber<Real> xi1_dn{0, 1}; // initial guess
2272  auto && order = secondary_elem_candidate->default_order();
2273  unsigned int current_iterate = 0, max_iterates = 10;
2274 
2275  VectorValue<DualNumber<Real>> normals(0);
2276 
2277  // Newton iteration loop - this to converge in 1 iteration when it
2278  // succeeds, and possibly two iterations when it converges to a
2279  // xi outside the reference element. I don't know any reason why it should
2280  // only take 1 iteration -- the Jacobian is not constant in general...
2281  do
2282  {
2284  for (MooseIndex(secondary_elem_candidate->n_nodes()) n = 0;
2285  n < secondary_elem_candidate->n_nodes();
2286  ++n)
2287  {
2288  const auto phi = Moose::fe_lagrange_1D_shape(order, n, xi1_dn);
2289  x1 += phi * secondary_elem_candidate->point(n);
2290  normals += phi * nodal_normals[n];
2291  }
2292 
2293  const auto u = x1 - (*primary_node);
2294 
2295  const auto F = u(0) * normals(1) - u(1) * normals(0);
2296 
2297  if (std::abs(F) < _newton_tolerance)
2298  break;
2299 
2300  // Unlike for projection of nodal normals onto primary surfaces, we should never have a
2301  // case where the nodal normal is completely orthogonal to the secondary surface, so we
2302  // do not have to guard against F.derivatives() == 0 here
2303  Real dxi1 = -F.value() / F.derivatives();
2304 
2305  xi1_dn += dxi1;
2306 
2307  normals = 0;
2308  } while (++current_iterate < max_iterates);
2309 
2310  Real xi1 = xi1_dn.value();
2311 
2312  // Check for convergence to a valid solution... The last condition checks for obliqueness
2313  // of the projection
2314  if ((current_iterate < max_iterates) && (std::abs(xi1) <= 1. + _xi_tolerance) &&
2315  (std::abs((primary_side_elem->point(0) - primary_side_elem->point(1)).unit() *
2316  MetaPhysicL::raw_value(normals).unit()) <
2318  {
2319  if (std::abs(std::abs(xi1) - 1.) < _xi_tolerance)
2320  {
2321  // Special case: xi1=+/-1.
2322  // It is unlikely that we get here, because this primary node should already
2323  // have been mapped during the project_secondary_nodes() routine, but
2324  // there is still a chance since the tolerances are applied to
2325  // the xi coordinate and that value may be different on a primary element and a
2326  // secondary element since they may have different sizes.
2327  throw MooseException("Nodes on primary and secondary surfaces are aligned. This is "
2328  "causing trouble when identifying projections from secondary "
2329  "nodes when performing primary node projections.");
2330  }
2331  else // somewhere in the middle of the Elem
2332  {
2333  // Add entry to primary_node_and_elem_to_xi1_secondary_elem
2334  //
2335  // Note: we originally duplicated the map values for the keys (node, left_neighbor)
2336  // and (node, right_neighbor) but I don't think that should be necessary. Instead we
2337  // just do it for neighbor 0, but really maybe we don't even need to do that since
2338  // we can always look up the neighbors later given the Node... keeping it like this
2339  // helps to maintain the "symmetry" of the two containers.
2340  const Elem * neigh = primary_node_neighbors[0];
2341  for (MooseIndex(neigh->n_vertices()) nid = 0; nid < neigh->n_vertices(); ++nid)
2342  {
2343  const Node * neigh_node = neigh->node_ptr(nid);
2344  if (primary_node == neigh_node)
2345  {
2346  auto key = std::make_tuple(neigh_node->id(), neigh_node, neigh);
2347  auto val = std::make_pair(xi1, secondary_elem_candidate);
2349  }
2350  }
2351  }
2352 
2353  projection_succeeded = true;
2354  break; // out of e-loop
2355  }
2356  else
2357  {
2358  // The current primary_point is not in this Elem, so keep track of the rejects.
2359  rejected_secondary_elem_candidates.insert(secondary_elem_candidate);
2360  }
2361  } // end e-loop over candidate elems
2362 
2363  if (projection_succeeded)
2364  break; // out of r-loop
2365  } // r-loop
2366 
2367  if (!projection_succeeded && _debug)
2368  {
2369  _console << "Failed to find point from which primary node "
2370  << static_cast<const Point &>(*primary_node) << " was projected." << std::endl
2371  << std::endl;
2372  }
2373  } // loop over side nodes
2374  } // end loop over elements for finding where primary points would have projected from.
2375 }
2376 
2377 std::vector<AutomaticMortarGeneration::MortarFilterIter>
2379 {
2380  auto secondary_it = _nodes_to_secondary_elem_map.find(node.id());
2381  if (secondary_it == _nodes_to_secondary_elem_map.end())
2382  return {};
2383 
2384  const auto & secondary_elems = secondary_it->second;
2385  std::vector<MortarFilterIter> ret;
2386  ret.reserve(secondary_elems.size());
2387 
2388  for (const auto i : index_range(secondary_elems))
2389  {
2390  auto * const secondary_elem = secondary_elems[i];
2391  auto msm_it = _secondary_elems_to_mortar_segments.find(secondary_elem->id());
2392  if (msm_it == _secondary_elems_to_mortar_segments.end())
2393  // We may have removed this element key from this map
2394  continue;
2395 
2396  mooseAssert(secondary_elem->active(),
2397  "We loop over active elements when building the mortar segment mesh, so we golly "
2398  "well hope this is active.");
2399  mooseAssert(!msm_it->second.empty(),
2400  "We should have removed all secondaries from this map if they do not have any "
2401  "mortar segments associated with them.");
2402  ret.push_back(msm_it);
2403  }
2404 
2405  return ret;
2406 }
virtual T maximum() const
std::set< SubdomainID > _primary_boundary_subdomain_ids
const Elem * getSecondaryLowerdElemFromSecondaryElem(dof_id_type secondary_elem_id) const
Return lower dimensional secondary element given its interior parent.
ElemType
unique_id_type & set_unique_id()
std::unordered_set< const Elem * > _inactive_local_lm_elems
List of inactive lagrange multiplier nodes (for elemental variables)
auto norm() const -> decltype(std::norm(Real()))
bool absoluteFuzzyEqual(const T &var1, const T2 &var2, const T3 &tol=libMesh::TOLERANCE *libMesh::TOLERANCE)
Function to check whether two variables are equal within an absolute tolerance.
Definition: MooseUtils.h:346
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
MPI_Info info
void clear()
Clears the mortar segment mesh and accompanying data structures.
virtual void write_equation_systems(const std::string &, const EquationSystems &, const std::set< std::string > *system_names=nullptr)
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:299
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 ...
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:494
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
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:333
void output() override
Overload this function with the desired output activities.
MortarNodalGeometryOutput(const InputParameters &params)
void push_back(const T &v)
Definition: MooseUtils.h:1112
auto raw_value(const Eigen::Map< T > &in)
Definition: ADReal.h:73
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:73
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()
Outputs mesh statistics for mortar segment mesh.
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
SECOND
Specialization of SubProblem for solving nonlinear equations plus auxiliary equations.
FEProblemBase & feProblem() const
Definition: MooseApp.C:1327
void projectSecondaryNodes()
Project secondary nodes (find xi^(2) values) to the closest points on the primary surface...
virtual EquationSystems & es()=0
TRI3
const std::unordered_map< dof_id_type, std::set< Elem *, CompareDofObjectsByID > > & secondariesToMortarSegments() const
Utility class template for a semidynamic vector with a maximum size N and a chosen dynamic size...
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.
ADRealEigenVector< T, D, asd > abs(const ADRealEigenVector< T, D, asd > &)
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
std::set< SubdomainID > _secondary_ip_sub_ids
All the secondary interior parent subdomain IDs associated with the mortar mesh.
processor_id_type n_processors() const
TypeVector< Real > unit() const
void libmesh_ignore(const Args &...)
void projectPrimaryNodes()
(Inverse) project primary nodes to the points on the secondary surface where they would have come fro...
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
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)
Must be constructed with a reference to the Mesh we are generating mortar spaces for.
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.
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...
An inteface for the _console for outputting to the Console object.
T sign(T x)
Definition: MathUtils.h:83
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.
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...
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...
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.
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:75
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.
const Node * node_ptr(const unsigned int i) const
auto norm_sq(const T &a) -> decltype(std::norm(a))
virtual void write(const std::string &fname) override
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)
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
T fe_lagrange_2D_shape(const ElemType type, const Order order, const unsigned int i, const VectorType< T > &p)
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:1773
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.