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