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