https://mooseframework.inl.gov
MortarSegmentHelper.C
Go to the documentation of this file.
1 //* This file is part of the MOOSE framework
2 //* https://mooseframework.inl.gov
3 //*
4 //* All rights reserved, see COPYRIGHT for full restrictions
5 //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6 //*
7 //* Licensed under LGPL 2.1, please see LICENSE for details
8 //* https://www.gnu.org/licenses/lgpl-2.1.html
9 #include "MortarSegmentHelper.h"
10 #include "MooseError.h"
11 #include "MooseTypes.h"
12 
13 #include "libmesh/fe_interface.h"
14 #include "libmesh/fe_map.h"
15 #include "libmesh/face_quad4.h"
16 #include "libmesh/face_tri3.h"
17 #include "libmesh/int_range.h"
18 #include "libmesh/node.h"
19 #include "libmesh/utility.h"
20 #if defined(LIBMESH_HAVE_TRIANGLE) || defined(LIBMESH_HAVE_POLY2TRI)
21 #include "libmesh/replicated_mesh.h"
22 #include "libmesh/mesh_triangle_interface.h"
23 #include "libmesh/poly2tri_triangulator.h"
24 #endif
25 
26 #include <algorithm>
27 #include <array>
28 #include <cmath>
29 #include <limits>
30 #include <map>
31 #include <numeric>
32 #include <optional>
33 #include <sstream>
34 #include <set>
35 #include <string>
36 #include <unordered_map>
37 #include <utility>
38 
39 using namespace libMesh;
40 
41 namespace
42 {
43 
44 constexpr Real mortar_reference_mapping_tolerance = 1e-8;
45 
46 bool
47 isFinitePoint(const Point & point)
48 {
49  for (const auto component : make_range(Moose::dim))
50  if (!std::isfinite(point(component)))
51  return false;
52 
53  return true;
54 }
55 
56 // Signed-area test for the 2D triangle (a, b, c). Returns twice the signed area:
57 // positive if a->b->c is counter-clockwise, negative if clockwise, zero if
58 // collinear. Used as the building block for orientation, point-in-triangle, and
59 // circumcircle predicates.
60 Real
61 orient2dHelper(const Point & a, const Point & b, const Point & c)
62 {
63  return (b(0) - a(0)) * (c(1) - a(1)) - (b(1) - a(1)) * (c(0) - a(0));
64 }
65 
66 Real
67 triangleAreaHelper(const Point & a, const Point & b, const Point & c)
68 {
69  return 0.5 * std::abs(orient2dHelper(a, b, c));
70 }
71 
72 // Canonical key for an undirected edge: the two endpoint indices sorted so that
73 // (a, b) and (b, a) hash and compare equal. Used to dedupe / look up edges in
74 // triangle-adjacency maps.
75 std::array<unsigned int, 2>
76 canonicalEdgeHelper(const unsigned int a, const unsigned int b)
77 {
78  return {{std::min(a, b), std::max(a, b)}};
79 }
80 
81 // Reorder the three vertex indices (a, b, c) so the resulting triangle is wound
82 // counter-clockwise (CCW) in the 2D plane spanned by \p nodes. Many of the
83 // triangulation paths (orientation tests, area accumulation, ear-clipping
84 // validity checks) assume CCW input, so we normalize before emitting triangles.
85 std::array<unsigned int, 3>
86 makeCCWTriangleHelper(const std::vector<Point> & nodes,
87  const unsigned int a,
88  const unsigned int b,
89  const unsigned int c)
90 {
91  if (orient2dHelper(nodes[a], nodes[b], nodes[c]) >= 0)
92  return {{a, b, c}};
93  return {{a, c, b}};
94 }
95 
96 bool
97 pointInCircumcircleHelper(const Point & a, const Point & b, const Point & c, const Point & p)
98 {
99  const auto ax = a(0) - p(0);
100  const auto ay = a(1) - p(1);
101  const auto bx = b(0) - p(0);
102  const auto by = b(1) - p(1);
103  const auto cx = c(0) - p(0);
104  const auto cy = c(1) - p(1);
105  const Real det = (ax * ax + ay * ay) * (bx * cy - by * cx) -
106  (bx * bx + by * by) * (ax * cy - ay * cx) +
107  (cx * cx + cy * cy) * (ax * by - ay * bx);
108  const Real orientation = orient2dHelper(a, b, c);
109  return orientation >= 0 ? det > TOLERANCE : det < -TOLERANCE;
110 }
111 
112 void
113 performLocalDelaunayFlips(const std::vector<Point> & poly_nodes,
114  const std::set<std::array<unsigned int, 2>> & constrained_edges,
115  std::vector<std::array<unsigned int, 3>> & triangles)
116 {
117  bool flipped = true;
118  while (flipped)
119  {
120  flipped = false;
121 
122  std::map<std::array<unsigned int, 2>, std::vector<unsigned int>> edge_to_triangles;
123  for (const auto tri_index : index_range(triangles))
124  {
125  const auto & tri = triangles[tri_index];
126  edge_to_triangles[canonicalEdgeHelper(tri[0], tri[1])].push_back(tri_index);
127  edge_to_triangles[canonicalEdgeHelper(tri[1], tri[2])].push_back(tri_index);
128  edge_to_triangles[canonicalEdgeHelper(tri[2], tri[0])].push_back(tri_index);
129  }
130 
131  for (const auto & [edge, owning_triangles] : edge_to_triangles)
132  {
133  if (owning_triangles.size() != 2 || constrained_edges.count(edge))
134  continue;
135 
136  const auto first_tri_index = owning_triangles[0];
137  const auto second_tri_index = owning_triangles[1];
138  const auto & first_triangle = triangles[first_tri_index];
139  const auto & second_triangle = triangles[second_tri_index];
140 
141  const auto a = edge[0];
142  const auto b = edge[1];
143  const auto first_opposite =
144  *std::find_if(first_triangle.begin(),
145  first_triangle.end(),
146  [a, b](const unsigned int vertex) { return vertex != a && vertex != b; });
147  const auto second_opposite =
148  *std::find_if(second_triangle.begin(),
149  second_triangle.end(),
150  [a, b](const unsigned int vertex) { return vertex != a && vertex != b; });
151 
152  if (first_opposite == second_opposite)
153  continue;
154 
155  const auto side_a =
156  orient2dHelper(poly_nodes[first_opposite], poly_nodes[second_opposite], poly_nodes[a]);
157  const auto side_b =
158  orient2dHelper(poly_nodes[first_opposite], poly_nodes[second_opposite], poly_nodes[b]);
159  if (side_a * side_b >= -TOLERANCE)
160  continue;
161 
162  if (!pointInCircumcircleHelper(poly_nodes[first_triangle[0]],
163  poly_nodes[first_triangle[1]],
164  poly_nodes[first_triangle[2]],
165  poly_nodes[second_opposite]))
166  continue;
167 
168  triangles[first_tri_index] =
169  makeCCWTriangleHelper(poly_nodes, first_opposite, second_opposite, b);
170  triangles[second_tri_index] =
171  makeCCWTriangleHelper(poly_nodes, second_opposite, first_opposite, a);
172  flipped = true;
173  break;
174  }
175  }
176 }
177 
178 #if defined(LIBMESH_HAVE_TRIANGLE) || defined(LIBMESH_HAVE_POLY2TRI)
179 void
180 triangulateConstrainedDelaunayPolygon(std::vector<Point> & poly_nodes,
181  const Real area_tol,
182  const Real length_tol,
183  std::vector<std::vector<unsigned int>> & tri_map)
184 {
185  Parallel::Communicator comm_self;
186  ReplicatedMesh triangulation_mesh(comm_self, 2);
187  std::unordered_map<dof_id_type, unsigned int> node_id_to_local_index;
188  node_id_to_local_index.reserve(poly_nodes.size());
189 
190  for (const auto i : index_range(poly_nodes))
191  triangulation_mesh.add_point(poly_nodes[i], i);
192 
193  triangulation_mesh.set_mesh_dimension(2);
194 
195 #ifdef LIBMESH_HAVE_TRIANGLE
196  TriangleInterface triangulator(triangulation_mesh);
197 #else
198  Poly2TriTriangulator triangulator(triangulation_mesh);
199  triangulator.set_refine_boundary_allowed(false);
200 #endif
201 
202  triangulator.triangulation_type() = TriangulatorInterface::PSLG;
203  triangulator.elem_type() = TRI3;
204  triangulator.set_interpolate_boundary_points(0);
205  triangulator.set_verify_hole_boundaries(false);
206  triangulator.desired_area() = 0;
207  triangulator.minimum_angle() = 0;
208  triangulator.smooth_after_generating() = false;
209  triangulator.quiet() = true;
210  triangulator.segments.reserve(poly_nodes.size());
211  for (const auto i : index_range(poly_nodes))
212  triangulator.segments.emplace_back(i, (i + 1) % poly_nodes.size());
213 
214  triangulator.triangulate();
215 
216  // node_ptr_range() and active_element_ptr_range() iterate in id order on this
217  // serial ReplicatedMesh, so no explicit sort is needed.
218  for (const auto * const node : triangulation_mesh.node_ptr_range())
219  if (!node_id_to_local_index.count(node->id()))
220  {
221  // Node inherits from Point and the triangulator operates on a 2D plane, so
222  // the libMesh node already lives at z = 0 and we can use it directly.
223  unsigned int matched_index = libMesh::invalid_uint;
224  Real best_distance = std::numeric_limits<Real>::max();
225 
226  for (const auto i : index_range(poly_nodes))
227  {
228  const Real distance = (*node - poly_nodes[i]).norm();
229  if (distance <= length_tol && distance < best_distance)
230  {
231  matched_index = i;
232  best_distance = distance;
233  }
234  }
235 
236  if (matched_index == libMesh::invalid_uint)
237  {
238  matched_index = cast_int<unsigned int>(poly_nodes.size());
239  poly_nodes.push_back(*node);
240  }
241 
242  node_id_to_local_index.emplace(node->id(), matched_index);
243  }
244 
245  std::vector<std::array<unsigned int, 3>> triangles;
246  triangles.reserve(triangulation_mesh.n_elem());
247 
248  for (const auto * const elem : triangulation_mesh.active_element_ptr_range())
249  {
250  mooseAssert(elem->type() == TRI3,
251  "The delaunay mortar triangulation backend produced a non-TRI3 element: "
252  << static_cast<int>(elem->type()));
253 
254  std::array<unsigned int, 3> local_triangle;
255  for (const auto i : index_range(local_triangle))
256  local_triangle[i] = libmesh_map_find(node_id_to_local_index, elem->node_id(i));
257 
258  const Real orientation = orient2dHelper(poly_nodes[local_triangle[0]],
259  poly_nodes[local_triangle[1]],
260  poly_nodes[local_triangle[2]]);
261  if (std::abs(orientation) <= 2. * area_tol)
262  continue;
263 
264  if (orientation < 0)
265  std::swap(local_triangle[1], local_triangle[2]);
266 
267  triangles.push_back(local_triangle);
268  }
269 
270  std::set<std::array<unsigned int, 2>> constrained_edges;
271  for (const auto i : index_range(poly_nodes))
272  constrained_edges.insert(canonicalEdgeHelper(i, (i + 1) % poly_nodes.size()));
273 
274  performLocalDelaunayFlips(poly_nodes, constrained_edges, triangles);
275 
276  std::set<std::array<unsigned int, 3>> seen_triangles;
277  for (auto local_triangle : triangles)
278  {
279  auto canonical_triangle = local_triangle;
280  std::sort(canonical_triangle.begin(), canonical_triangle.end());
281  if (!seen_triangles.insert(canonical_triangle).second)
282  continue;
283 
284  tri_map.push_back({local_triangle[0], local_triangle[1], local_triangle[2]});
285  }
286 }
287 #endif
288 
289 } // namespace
290 
291 MortarSegmentHelper::MortarSegmentHelper(std::vector<Point> secondary_nodes,
292  const Point & center,
293  const Point & normal,
294  const MortarSegmentTriangulationMode triangulation_mode,
295  const bool triangulate_triangles)
297  std::move(secondary_nodes), {}, center, normal, triangulation_mode, triangulate_triangles)
298 {
299 }
300 
301 MortarSegmentHelper::MortarSegmentHelper(std::vector<Point> secondary_nodes,
302  std::vector<Point> secondary_reference_points,
303  const Point & center,
304  const Point & normal,
305  const MortarSegmentTriangulationMode triangulation_mode,
306  const bool triangulate_triangles)
307  : _center(center),
308  _normal(normal),
309  _debug(false),
310  _triangulation_mode(triangulation_mode),
311  _triangulate_triangles(triangulate_triangles),
312  _secondary_reference_points(std::move(secondary_reference_points))
313 {
314  mooseAssert(_secondary_reference_points.empty() ||
315  secondary_nodes.size() == _secondary_reference_points.size(),
316  "Each projected secondary node needs one parent reference point.");
317 
318  _secondary_poly.clear();
319  _secondary_poly.reserve(secondary_nodes.size());
320 
321  // Get orientation of secondary poly
322  const Point e1 = secondary_nodes[0] - secondary_nodes[1];
323  const Point e2 = secondary_nodes[2] - secondary_nodes[1];
324  const Real orient = e2.cross(e1) * _normal;
325 
326  // u and v define the tangent plane of the element (at center)
327  // Note we embed orientation into our transformation to make 2D poly always
328  // positively oriented
329  _u = _normal.cross(secondary_nodes[0] - center).unit();
330  _v = (orient > 0) ? _normal.cross(_u).unit() : _u.cross(_normal).unit();
331 
332  // Transform problem to 2D plane spanned by u and v
333  for (const auto & node : secondary_nodes)
334  {
335  Point pt = node - _center;
336  _secondary_poly.emplace_back(pt * _u, pt * _v, 0);
337  }
338 
339  // Initialize area of secondary polygon
342 
343  // Tolerance for quantities with area dimensions
345 
346  // Tolerance for quantites with length dimensions
348 }
349 
350 Point
352  const Point & p1, const Point & p2, const Point & q1, const Point & q2, Real & s) const
353 {
354  const Point dp = p2 - p1;
355  const Point dq = q2 - q1;
356  const Real cp1q1 = p1(0) * q1(1) - p1(1) * q1(0);
357  const Real cp1q2 = p1(0) * q2(1) - p1(1) * q2(0);
358  const Real cq1q2 = q1(0) * q2(1) - q1(1) * q2(0);
359  const Real alpha = 1. / (dp(0) * dq(1) - dp(1) * dq(0));
360  s = -alpha * (cp1q2 - cp1q1 - cq1q2);
361 
362  // Intersection should be between p1 and p2, if it's not (due to poor conditioning), simply
363  // move it to one of the end points
364  s = s > 1 ? 1. : s;
365  s = s < 0 ? 0. : s;
366  return p1 + s * dp;
367 }
368 
369 bool
371 {
372  for (auto i : index_range(_secondary_poly))
373  {
374  const Point & q1 = _secondary_poly[i];
375  const Point & q2 = _secondary_poly[(i + 1) % _secondary_poly.size()];
376 
377  const Point e1 = q2 - q1;
378  const Point e2 = pt - q1;
379 
380  // If point corresponds to one of the secondary vertices, skip
381  if (e2.norm() < _tolerance)
382  return true;
383 
384  const bool inside = (e1(0) * e2(1) - e1(1) * e2(0)) < _area_tol;
385  if (!inside)
386  return false;
387  }
388  return true;
389 }
390 
391 bool
392 MortarSegmentHelper::isDisjoint(const std::vector<Point> & poly) const
393 {
394  for (auto i : index_range(_secondary_poly))
395  {
396  // Get edge to check
397  const Point & q1 = _secondary_poly[i];
398  const Point & q2 = _secondary_poly[(i + 1) % _secondary_poly.size()];
399  const Point edg = q2 - q1;
400  const Real cp = q2(0) * q1(1) - q2(1) * q1(0);
401 
402  // If more optimization needed, could store these values for later
403  // Check if point is to the left of (or on) clip_edge
404  auto is_inside = [&edg, cp](Point & pt, Real tol)
405  { return pt(0) * edg(1) - pt(1) * edg(0) + cp < -tol; };
406 
407  bool all_outside = true;
408  for (auto pt : poly)
409  if (is_inside(pt, _area_tol))
410  all_outside = false;
411 
412  if (all_outside)
413  return true;
414  }
415  return false;
416 }
417 
418 std::vector<Point>
419 MortarSegmentHelper::projectPrimaryPoly(const std::vector<Point> & primary_nodes) const
420 {
421  // Check orientation of primary_poly
422  const Point e1 = primary_nodes[0] - primary_nodes[1];
423  const Point e2 = primary_nodes[2] - primary_nodes[1];
424 
425  // Note we use u x v here instead of normal because it may be flipped if secondary elem was
426  // negatively oriented
427  const Real orient = e2.cross(e1) * _u.cross(_v);
428 
429  // Get primary_poly (primary is clipping poly). If negatively oriented, reverse
430  std::vector<Point> primary_poly;
431  const int n_verts = primary_nodes.size();
432  primary_poly.reserve(primary_nodes.size());
433  for (auto n : index_range(primary_nodes))
434  {
435  Point pt = (orient > 0) ? primary_nodes[n] - _center : primary_nodes[n_verts - 1 - n] - _center;
436  primary_poly.emplace_back(pt * _u, pt * _v, 0.);
437  }
438 
439  return primary_poly;
440 }
441 
442 std::vector<Point>
443 MortarSegmentHelper::clipPoly(const std::vector<Point> & primary_nodes) const
444 {
445  return clipProjectedPoly(projectPrimaryPoly(primary_nodes));
446 }
447 
448 std::vector<Point>
449 MortarSegmentHelper::clipProjectedPoly(const std::vector<Point> & primary_poly) const
450 {
451  if (isDisjoint(primary_poly))
452  return {};
453 
454  // Initialize clipped poly with secondary poly (secondary is target poly)
455  std::vector<Point> clipped_poly = _secondary_poly;
456 
457  // Loop through clipping edges
458  for (auto i : index_range(primary_poly))
459  {
460  // If clipped poly trivial, return
461  if (clipped_poly.size() < 3)
462  {
463  clipped_poly.clear();
464  return clipped_poly;
465  }
466 
467  // Set input poly to current clipped poly
468  std::vector<Point> input_poly(clipped_poly);
469  clipped_poly.clear();
470 
471  // Get clipping edge
472  const Point & clip_pt1 = primary_poly[i];
473  const Point & clip_pt2 = primary_poly[(i + 1) % primary_poly.size()];
474  const Point edg = clip_pt2 - clip_pt1;
475  const Real cp = clip_pt2(0) * clip_pt1(1) - clip_pt2(1) * clip_pt1(0);
476 
477  // Check if point is to the left of (or on) clip_edge
478  /*
479  * Note that use of tolerance here is to avoid degenerate case when lines are
480  * essentially on top of each other (common when meshes match across interface)
481  * since finding intersection is ill-conditioned in this case.
482  */
483  auto is_inside = [&edg, cp](const Point & pt, Real tol)
484  { return pt(0) * edg(1) - pt(1) * edg(0) + cp < tol; };
485 
486  // Loop through edges of target polygon (with previous clippings already included)
487  for (auto j : index_range(input_poly))
488  {
489  // Get target edge
490  const Point curr_pt = input_poly[(j + 1) % input_poly.size()];
491  const Point prev_pt = input_poly[j];
492 
493  // TODO: Don't need to calculate both each loop
494  const bool is_current_inside = is_inside(curr_pt, _area_tol);
495  const bool is_previous_inside = is_inside(prev_pt, _area_tol);
496 
497  if (is_current_inside)
498  {
499  if (!is_previous_inside)
500  {
501  Real s;
502  Point intersect = getIntersection(prev_pt, curr_pt, clip_pt1, clip_pt2, s);
503 
504  /*
505  * s is the fraction of distance along clip poly edge that intersection lies
506  * It is used here to avoid degenerate polygon cases. For example, consider a
507  * case like:
508  * o
509  * | (inside)
510  * ------|------
511  * | (outside)
512  * when the distance is small (< 1e-7) we don't want to to add both the point
513  * and intersection. Also note that when distance on the scale of 1e-7,
514  * area on scale of 1e-14 so is insignificant if this results in dropping
515  * a tri (for example if next edge crosses again)
516  */
517  if (s < (1 - _tolerance))
518  clipped_poly.push_back(intersect);
519  }
520  clipped_poly.push_back(curr_pt);
521  }
522  else if (is_previous_inside)
523  {
524  Real s;
525  Point intersect = getIntersection(prev_pt, curr_pt, clip_pt1, clip_pt2, s);
526  if (s > _tolerance)
527  clipped_poly.push_back(intersect);
528  }
529  }
530  }
531 
532  // Make sure final clipped poly is not trivial
533  if (clipped_poly.size() < 3)
534  {
535  clipped_poly.clear();
536  return clipped_poly;
537  }
538 
539  // Clean up result by removing any duplicate nodes
540  std::vector<Point> cleaned_poly;
541  cleaned_poly.push_back(clipped_poly.back());
542  for (auto i : make_range(clipped_poly.size() - 1))
543  {
544  const Point prev_pt = cleaned_poly.back();
545  const Point curr_pt = clipped_poly[i];
546 
547  // If points are sufficiently distanced, add to output
548  if ((curr_pt - prev_pt).norm() > _length_tol)
549  cleaned_poly.push_back(curr_pt);
550  }
551 
552  mooseAssert(
553  cleaned_poly.size() <= 8,
554  "Our distributed mesh numbering scheme assumes that we have at most 8 nodes resulting from "
555  "clipping the projection of the primary sub-element onto the secondary sub-element");
556  return cleaned_poly;
557 }
558 
559 void
560 MortarSegmentHelper::triangulatePoly(std::vector<Point> & poly_nodes,
561  std::vector<std::vector<unsigned int>> & tri_map) const
562 {
563  // tri_map is populated with triangle indices that are local to poly_nodes (starting at 0).
564  // Callers are responsible for shifting these indices into a global node numbering.
565  const auto polygon_centroid = [](const std::vector<Point> & polygon_nodes)
566  {
567  Point centroid(0);
568  Real double_area = 0;
569  for (const auto i : index_range(polygon_nodes))
570  {
571  const auto & a = polygon_nodes[i];
572  const auto & b = polygon_nodes[(i + 1) % polygon_nodes.size()];
573  const Real cross = a(0) * b(1) - b(0) * a(1);
574  double_area += cross;
575  centroid(0) += (a(0) + b(0)) * cross;
576  centroid(1) += (a(1) + b(1)) * cross;
577  }
578 
579  if (std::abs(double_area) <= TOLERANCE)
580  {
581  for (const auto & node : polygon_nodes)
582  centroid += node;
583  centroid /= polygon_nodes.size();
584  return centroid;
585  }
586 
587  centroid /= (3. * double_area);
588  centroid(2) = 0;
589  return centroid;
590  };
591 
592  const auto append_triangle = [this, &poly_nodes, &tri_map](
593  const unsigned int a, const unsigned int b, const unsigned int c)
594  {
595  if (triangleAreaHelper(poly_nodes[a], poly_nodes[b], poly_nodes[c]) <= _area_tol)
596  return false;
597 
598  if (orient2dHelper(poly_nodes[a], poly_nodes[b], poly_nodes[c]) >= 0)
599  tri_map.push_back({a, b, c});
600  else
601  tri_map.push_back({a, c, b});
602 
603  return true;
604  };
605 
606  const auto point_in_triangle =
607  [this](const Point & p, const Point & a, const Point & b, const Point & c)
608  {
609  const Real ab = orient2dHelper(a, b, p);
610  const Real bc = orient2dHelper(b, c, p);
611  const Real ca = orient2dHelper(c, a, p);
612  return ab >= -_area_tol && bc >= -_area_tol && ca >= -_area_tol;
613  };
614 
615  const auto min_triangle_angle = [](const Point & a, const Point & b, const Point & c)
616  {
617  const auto clamp_cos = [](Real value) { return std::max(-1., std::min(1., value)); };
618  const auto angle_at =
619  [&clamp_cos](const Point & vertex, const Point & point_one, const Point & point_two)
620  {
621  const Point edge_one = point_one - vertex;
622  const Point edge_two = point_two - vertex;
623  const Real denom = edge_one.norm() * edge_two.norm();
624  if (denom <= TOLERANCE)
625  return 0.;
626  return std::acos(clamp_cos((edge_one * edge_two) / denom));
627  };
628 
629  return std::min({angle_at(a, b, c), angle_at(b, c, a), angle_at(c, a, b)});
630  };
631 
632  const auto canonicalize_polygon = [this, &poly_nodes]()
633  {
634  if (poly_nodes.size() < 3)
635  return;
636 
637  if (area(poly_nodes) < 0)
638  std::reverse(poly_nodes.begin(), poly_nodes.end());
639 
640  bool changed = true;
641  while (changed && poly_nodes.size() > 3)
642  {
643  changed = false;
644  for (const auto i : index_range(poly_nodes))
645  {
646  const auto prev = (i + poly_nodes.size() - 1) % poly_nodes.size();
647  const auto next = (i + 1) % poly_nodes.size();
648  if ((poly_nodes[i] - poly_nodes[prev]).norm() <= _length_tol ||
649  (poly_nodes[next] - poly_nodes[i]).norm() <= _length_tol ||
650  triangleAreaHelper(poly_nodes[prev], poly_nodes[i], poly_nodes[next]) <= _area_tol)
651  {
652  poly_nodes.erase(poly_nodes.begin() + i);
653  changed = true;
654  break;
655  }
656  }
657  }
658 
659  if (poly_nodes.size() >= 3 && area(poly_nodes) < 0)
660  std::reverse(poly_nodes.begin(), poly_nodes.end());
661  };
662 
663  const auto triangulate_with_ear_clipping =
664  [this, &poly_nodes, &point_in_triangle, &min_triangle_angle](
665  const bool perform_delaunay_flips)
666  {
667  std::vector<std::array<unsigned int, 3>> triangles;
668  if (poly_nodes.size() < 3)
669  return triangles;
670 
671  if (poly_nodes.size() == 3)
672  {
673  triangles.push_back(makeCCWTriangleHelper(poly_nodes, 0, 1, 2));
674  return triangles;
675  }
676 
677  std::vector<unsigned int> remaining_vertices(poly_nodes.size());
678  std::iota(remaining_vertices.begin(), remaining_vertices.end(), 0);
679 
680  while (remaining_vertices.size() > 3)
681  {
682  std::optional<std::size_t> best_position;
683  Real best_score = -std::numeric_limits<Real>::max();
684  Real best_area = -std::numeric_limits<Real>::max();
685 
686  for (const auto position : index_range(remaining_vertices))
687  {
688  const auto prev_position =
689  (position + remaining_vertices.size() - 1) % remaining_vertices.size();
690  const auto next_position = (position + 1) % remaining_vertices.size();
691  const auto prev = remaining_vertices[prev_position];
692  const auto curr = remaining_vertices[position];
693  const auto next = remaining_vertices[next_position];
694 
695  if (orient2dHelper(poly_nodes[prev], poly_nodes[curr], poly_nodes[next]) <= _area_tol)
696  continue;
697 
698  bool contains_other_vertex = false;
699  for (const auto other : remaining_vertices)
700  {
701  if (other == prev || other == curr || other == next)
702  continue;
703 
704  if (point_in_triangle(
705  poly_nodes[other], poly_nodes[prev], poly_nodes[curr], poly_nodes[next]))
706  {
707  contains_other_vertex = true;
708  break;
709  }
710  }
711 
712  if (contains_other_vertex)
713  continue;
714 
715  const Real candidate_score =
716  min_triangle_angle(poly_nodes[prev], poly_nodes[curr], poly_nodes[next]);
717  const Real candidate_area =
718  triangleAreaHelper(poly_nodes[prev], poly_nodes[curr], poly_nodes[next]);
719  if (!best_position || candidate_score > best_score + TOLERANCE ||
720  (std::abs(candidate_score - best_score) <= TOLERANCE &&
721  candidate_area > best_area + _area_tol))
722  {
723  best_position = position;
724  best_score = candidate_score;
725  best_area = candidate_area;
726  }
727  }
728 
729  if (!best_position)
730  {
731  std::vector<std::array<unsigned int, 3>> best_fan;
732  Real best_fan_score = -std::numeric_limits<Real>::max();
733  Real best_fan_area = -std::numeric_limits<Real>::max();
734 
735  for (const auto root_position : index_range(remaining_vertices))
736  {
737  std::vector<std::array<unsigned int, 3>> candidate_fan;
738  Real candidate_score = std::numeric_limits<Real>::max();
739  Real candidate_area = std::numeric_limits<Real>::max();
740  bool valid_fan = true;
741  const auto root = remaining_vertices[root_position];
742 
743  for (unsigned int step = 1; step + 1 < remaining_vertices.size(); ++step)
744  {
745  const auto next_position = (root_position + step) % remaining_vertices.size();
746  const auto following_position = (root_position + step + 1) % remaining_vertices.size();
747  const auto vertex_one = remaining_vertices[next_position];
748  const auto vertex_two = remaining_vertices[following_position];
749 
750  if (orient2dHelper(poly_nodes[root], poly_nodes[vertex_one], poly_nodes[vertex_two]) <=
751  _area_tol)
752  {
753  valid_fan = false;
754  break;
755  }
756 
757  candidate_fan.push_back(
758  makeCCWTriangleHelper(poly_nodes, root, vertex_one, vertex_two));
759  candidate_score =
760  std::min(candidate_score,
761  min_triangle_angle(
762  poly_nodes[root], poly_nodes[vertex_one], poly_nodes[vertex_two]));
763  candidate_area =
764  std::min(candidate_area,
765  triangleAreaHelper(
766  poly_nodes[root], poly_nodes[vertex_one], poly_nodes[vertex_two]));
767  }
768 
769  if (!valid_fan || candidate_fan.empty())
770  continue;
771 
772  if (candidate_score > best_fan_score + TOLERANCE ||
773  (std::abs(candidate_score - best_fan_score) <= TOLERANCE &&
774  candidate_area > best_fan_area + _area_tol))
775  {
776  best_fan = std::move(candidate_fan);
777  best_fan_score = candidate_score;
778  best_fan_area = candidate_area;
779  }
780  }
781 
782  if (best_fan.empty())
783  for (unsigned int i = 1; i + 1 < remaining_vertices.size(); ++i)
784  best_fan.push_back(makeCCWTriangleHelper(poly_nodes,
785  remaining_vertices[0],
786  remaining_vertices[i],
787  remaining_vertices[i + 1]));
788 
789  triangles.insert(triangles.end(), best_fan.begin(), best_fan.end());
790  break;
791  }
792 
793  const auto prev_position =
794  (*best_position + remaining_vertices.size() - 1) % remaining_vertices.size();
795  const auto next_position = (*best_position + 1) % remaining_vertices.size();
796  triangles.push_back(makeCCWTriangleHelper(poly_nodes,
797  remaining_vertices[prev_position],
798  remaining_vertices[*best_position],
799  remaining_vertices[next_position]));
800  remaining_vertices.erase(remaining_vertices.begin() + *best_position);
801  }
802 
803  if (remaining_vertices.size() == 3)
804  triangles.push_back(makeCCWTriangleHelper(
805  poly_nodes, remaining_vertices[0], remaining_vertices[1], remaining_vertices[2]));
806 
807  if (!perform_delaunay_flips)
808  return triangles;
809 
810  std::set<std::array<unsigned int, 2>> boundary_edges;
811  for (const auto i : index_range(poly_nodes))
812  boundary_edges.insert(canonicalEdgeHelper(i, (i + 1) % poly_nodes.size()));
813 
814  performLocalDelaunayFlips(poly_nodes, boundary_edges, triangles);
815  return triangles;
816  };
817 
818  const auto is_convex_polygon = [this](const std::vector<Point> & polygon_nodes)
819  {
820  if (polygon_nodes.size() <= 3)
821  return true;
822 
823  for (const auto i : index_range(polygon_nodes))
824  {
825  const auto prev = (i + polygon_nodes.size() - 1) % polygon_nodes.size();
826  const auto next = (i + 1) % polygon_nodes.size();
827  if (orient2dHelper(polygon_nodes[prev], polygon_nodes[i], polygon_nodes[next]) <= _area_tol)
828  return false;
829  }
830 
831  return true;
832  };
833 
834  // Fewer than 3 nodes can't be triangulated
835  if (poly_nodes.size() < 3)
836  mooseError("Can't triangulate poly with fewer than 3 nodes");
837 
838  // Legacy centroid path: when the default triangulation (centroid) is selected
839  // and triangle re-tessellation is not requested, reproduce the legacy
840  // algorithm byte-for-byte so existing mortar baselines remain valid.
841  // Uses the arithmetic mean of the vertices (not the area-weighted centroid),
842  // emits one triangle per polygon edge without degeneracy filtering, and skips
843  // the canonicalization pass which would drop near-degenerate vertices and
844  // perturb integration weights in downstream test baselines.
846  {
847  if (poly_nodes.size() == 3)
848  {
849  tri_map.push_back({0, 1, 2});
850  return;
851  }
852 
853  const unsigned int n_verts = poly_nodes.size();
854  Point poly_center;
855  for (const auto & node : poly_nodes)
856  poly_center += node;
857  poly_center /= n_verts;
858 
859  for (const auto i : make_range(n_verts))
860  tri_map.push_back({i, (i + 1) % n_verts, n_verts});
861 
862  poly_nodes.push_back(poly_center);
863  return;
864  }
865 
866  canonicalize_polygon();
867  if (poly_nodes.size() < 3)
868  return;
869 
870  if (poly_nodes.size() == 3 && !_triangulate_triangles)
871  {
872  append_triangle(0, 1, 2);
873  return;
874  }
875 
876  const bool force_triangle_centroid_split = _triangulate_triangles && poly_nodes.size() == 3;
877 
879  !force_triangle_centroid_split)
880  {
881  const unsigned int n_verts = poly_nodes.size();
882  for (unsigned int i = 1; i + 1 < n_verts; ++i)
883  append_triangle(0, i, i + 1);
884  return;
885  }
886 
888  !force_triangle_centroid_split)
889  {
890 #if defined(LIBMESH_HAVE_TRIANGLE) || defined(LIBMESH_HAVE_POLY2TRI)
891  triangulateConstrainedDelaunayPolygon(poly_nodes, _area_tol, _length_tol, tri_map);
892  return;
893 #else
894  mooseError("The 'delaunay' mortar triangulation mode requires libMesh TriangleInterface or "
895  "Poly2Tri support.");
896 #endif
897  }
898 
900  !force_triangle_centroid_split)
901  {
902  for (const auto & triangle : triangulate_with_ear_clipping(true))
903  append_triangle(triangle[0], triangle[1], triangle[2]);
904  return;
905  }
906 
907  if (!force_triangle_centroid_split && !is_convex_polygon(poly_nodes))
908  {
909  for (const auto & triangle : triangulate_with_ear_clipping(true))
910  append_triangle(triangle[0], triangle[1], triangle[2]);
911  return;
912  }
913 
914  const unsigned int n_verts = poly_nodes.size();
915  const Point poly_center = polygon_centroid(poly_nodes);
916 
917  bool added_triangle = false;
918  for (const auto i : make_range(n_verts))
919  if (triangleAreaHelper(poly_nodes[i], poly_nodes[(i + 1) % n_verts], poly_center) > _area_tol)
920  {
921  tri_map.push_back({i, (i + 1) % n_verts, n_verts});
922  added_triangle = true;
923  }
924 
925  if (added_triangle)
926  poly_nodes.push_back(poly_center);
927 }
928 
929 void
930 MortarSegmentHelper::getMortarSegments(const std::vector<Point> & primary_nodes,
931  std::vector<Point> & nodes,
932  std::vector<std::vector<unsigned int>> & elem_to_nodes)
933 {
934  getMortarSegmentsImpl(primary_nodes, nodes, elem_to_nodes, nullptr);
935 }
936 
937 void
939  const std::vector<Point> & primary_nodes,
940  const std::vector<Point> & primary_reference_points,
941  std::vector<Point> & nodes,
942  std::vector<std::vector<unsigned int>> & elem_to_nodes,
943  std::vector<std::array<Point, 3>> & elem_to_secondary_reference_points,
944  std::vector<std::array<Point, 3>> & elem_to_primary_reference_points,
945  const Real minimum_segment_area)
946 {
947  ReferenceMappingData reference_mapping{primary_reference_points,
948  elem_to_secondary_reference_points,
949  elem_to_primary_reference_points,
950  minimum_segment_area};
951  getMortarSegmentsImpl(primary_nodes, nodes, elem_to_nodes, &reference_mapping);
952 }
953 
954 void
955 MortarSegmentHelper::getMortarSegmentsImpl(const std::vector<Point> & primary_nodes,
956  std::vector<Point> & nodes,
957  std::vector<std::vector<unsigned int>> & elem_to_nodes,
958  ReferenceMappingData * const reference_mapping)
959 {
960  std::vector<Point> primary_poly;
961  std::vector<Point> primary_poly_reference_points;
962 
963  if (reference_mapping)
964  {
965  if (primary_nodes.size() != reference_mapping->primary_reference_points.size())
966  mooseError("Reference-interpolation mortar segment generation requires one primary "
967  "reference point per primary sub-element node.");
968  if (_secondary_poly.size() != _secondary_reference_points.size())
969  mooseError("Reference-interpolation mortar segment generation requires one secondary "
970  "reference point per secondary sub-element node.");
971  if (reference_mapping->elem_to_secondary_reference_points.size() != elem_to_nodes.size() ||
972  reference_mapping->elem_to_primary_reference_points.size() != elem_to_nodes.size())
973  mooseError("Reference-interpolation mortar segment outputs must be aligned before appending "
974  "new segments.");
975 
976  // Keep reference points in the projected polygon's orientation.
977  const Point e1 = primary_nodes[0] - primary_nodes[1];
978  const Point e2 = primary_nodes[2] - primary_nodes[1];
979  const Real orient = e2.cross(e1) * _u.cross(_v);
980  const auto n_verts = primary_nodes.size();
981 
982  primary_poly = projectPrimaryPoly(primary_nodes);
983  primary_poly_reference_points.reserve(reference_mapping->primary_reference_points.size());
984  for (const auto n : index_range(primary_nodes))
985  {
986  const auto primary_node_index = (orient > 0) ? n : n_verts - 1 - n;
987  primary_poly_reference_points.push_back(
988  reference_mapping->primary_reference_points[primary_node_index]);
989  }
990  }
991 
992  // Clip primary elem against secondary elem. Reference mode preserves the projected primary
993  // ordering so its reference points remain aligned.
994  std::vector<Point> clipped_poly =
995  reference_mapping ? clipProjectedPoly(primary_poly) : clipPoly(primary_nodes);
996  if (clipped_poly.size() < 3)
997  return;
998 
999  if (_debug)
1000  for (const auto & point : clipped_poly)
1001  if (!isInsideSecondary(point))
1002  mooseError("Clipped polygon not inside linearized secondary element");
1003 
1004  // Compute area of clipped polygon, update remaining area fraction
1005  _remaining_area_fraction -= area(clipped_poly) / _secondary_area;
1006 
1007  // Triangulate clip polygon. tri_map indices are local to clipped_poly (starting at 0); we
1008  // shift them into the global node numbering after appending the polygon nodes below.
1009  std::vector<std::vector<unsigned int>> tri_map;
1010  triangulatePoly(clipped_poly, tri_map);
1011  if (reference_mapping && reference_mapping->minimum_segment_area > 0.)
1012  tri_map.erase(
1013  std::remove_if(tri_map.begin(),
1014  tri_map.end(),
1015  [&clipped_poly, reference_mapping](const std::vector<unsigned int> & tri)
1016  {
1017  mooseAssert(tri.size() == 3,
1018  "Mortar segment triangulation should only produce TRI3 maps.");
1019  return triangleAreaHelper(clipped_poly[tri[0]],
1020  clipped_poly[tri[1]],
1021  clipped_poly[tri[2]]) <
1022  reference_mapping->minimum_segment_area;
1023  }),
1024  tri_map.end());
1025  if (tri_map.empty())
1026  return;
1027 
1028  std::vector<Point> secondary_node_reference_points;
1029  std::vector<Point> primary_node_reference_points;
1030  if (reference_mapping)
1031  {
1032  secondary_node_reference_points.reserve(clipped_poly.size());
1033  primary_node_reference_points.reserve(clipped_poly.size());
1034 
1035  const auto recover_reference_point = [this](const Point & projected_point,
1036  const std::vector<Point> & poly,
1037  const std::vector<Point> & reference_points,
1038  const char * const parent_name,
1039  const std::size_t node_index)
1040  {
1041  std::string failure_reason;
1042  const auto reference_point =
1043  referencePoint(projected_point, poly, reference_points, &failure_reason);
1044  if (!reference_point)
1045  mooseError("Unable to recover the ",
1046  parent_name,
1047  " parent reference point for retained 3D mortar overlap vertex ",
1048  node_index,
1049  " at projected point ",
1050  projected_point,
1051  ". Reason: ",
1052  failure_reason,
1053  ". Reference interpolation does not fall back to normal projection.");
1054 
1055  return *reference_point;
1056  };
1057 
1058  for (const auto node_index : index_range(clipped_poly))
1059  {
1060  const auto & point = clipped_poly[node_index];
1061  secondary_node_reference_points.push_back(recover_reference_point(
1062  point, _secondary_poly, _secondary_reference_points, "secondary", node_index));
1063  primary_node_reference_points.push_back(recover_reference_point(
1064  point, primary_poly, primary_poly_reference_points, "primary", node_index));
1065  }
1066  }
1067 
1068  // Transform clipped poly back to (linearized) 3d and append to list
1069  const auto offset = cast_int<unsigned int>(nodes.size());
1070  for (const auto & point : clipped_poly)
1071  nodes.emplace_back((point(0) * _u) + (point(1) * _v) + _center);
1072 
1073  for (const auto & tri : tri_map)
1074  {
1075  std::vector<unsigned int> shifted_tri;
1076  shifted_tri.reserve(tri.size());
1077  for (const auto local_index : tri)
1078  shifted_tri.push_back(offset + local_index);
1079  elem_to_nodes.push_back(std::move(shifted_tri));
1080 
1081  if (reference_mapping)
1082  {
1083  mooseAssert(tri.size() == 3, "Mortar segment triangulation should only produce TRI3 maps.");
1084  std::array<Point, 3> elem_secondary_reference_points;
1085  std::array<Point, 3> elem_primary_reference_points;
1086  for (const auto n : index_range(tri))
1087  {
1088  const auto local_node = tri[n];
1089  elem_secondary_reference_points[n] = secondary_node_reference_points[local_node];
1090  elem_primary_reference_points[n] = primary_node_reference_points[local_node];
1091  }
1092 
1093  reference_mapping->elem_to_secondary_reference_points.push_back(
1094  elem_secondary_reference_points);
1095  reference_mapping->elem_to_primary_reference_points.push_back(elem_primary_reference_points);
1096  }
1097  }
1098 }
1099 
1100 std::optional<Point>
1102  const std::vector<Point> & poly,
1103  const std::vector<Point> & reference_points,
1104  std::string * const failure_reason) const
1105 {
1106  mooseAssert(poly.size() == reference_points.size(),
1107  "Projected point and reference point containers should be the same size.");
1108 
1109  if (failure_reason)
1110  failure_reason->clear();
1111 
1112  const auto fail = [failure_reason](const std::string & reason) -> std::optional<Point>
1113  {
1114  if (failure_reason)
1115  *failure_reason = reason;
1116  return std::nullopt;
1117  };
1118 
1119  if (!isFinitePoint(point))
1120  return fail("the projected target point contains a non-finite coordinate");
1121 
1122  for (const auto i : index_range(poly))
1123  {
1124  if (!isFinitePoint(poly[i]))
1125  return fail("projected polygon vertex " + std::to_string(i) +
1126  " contains a non-finite coordinate");
1127  if (!isFinitePoint(reference_points[i]))
1128  return fail("parent reference vertex " + std::to_string(i) +
1129  " contains a non-finite coordinate");
1130  }
1131 
1132  if (poly.size() != 3 && poly.size() != 4)
1133  return fail("reference point recovery only supports triangular and quadrilateral mortar "
1134  "sub-elements, but received " +
1135  std::to_string(poly.size()) + " vertices");
1136 
1137  Real minimum_edge_length = std::numeric_limits<Real>::max();
1138  Point local_origin;
1139  for (const auto & vertex : poly)
1140  local_origin += vertex;
1141  local_origin /= poly.size();
1142 
1143  Real local_scale = 0.;
1144  for (const auto i : index_range(poly))
1145  {
1146  minimum_edge_length =
1147  std::min(minimum_edge_length, (poly[(i + 1) % poly.size()] - poly[i]).norm());
1148  local_scale = std::max(local_scale, (poly[i] - local_origin).norm());
1149  }
1150 
1151  const Real singular_tolerance = 100. * std::numeric_limits<Real>::epsilon();
1152  if (!std::isfinite(local_scale) || local_scale <= singular_tolerance)
1153  return fail("the projected polygon has a zero local length scale");
1154  if (!std::isfinite(minimum_edge_length) ||
1155  minimum_edge_length / local_scale <= singular_tolerance)
1156  return fail("the projected polygon has a zero-length edge relative to its local scale");
1157 
1158  std::vector<Point> normalized_poly;
1159  normalized_poly.reserve(poly.size());
1160  for (const auto & vertex : poly)
1161  normalized_poly.push_back((vertex - local_origin) / local_scale);
1162  const Point normalized_point = (point - local_origin) / local_scale;
1163  const Real reference_tolerance =
1164  std::max(mortar_reference_mapping_tolerance, _area_tol / (minimum_edge_length * local_scale));
1165  std::array<Node, 4> element_nodes;
1166  const FEType fe_type(FIRST, LAGRANGE);
1167  const auto recover_with_libmesh = [&](auto & element) -> std::optional<Point>
1168  {
1169  for (const auto i : index_range(normalized_poly))
1170  {
1171  element_nodes[i] = normalized_poly[i];
1172  element_nodes[i].set_id(i);
1173  element.set_node(i, &element_nodes[i]);
1174  }
1175 
1176  if (!element.has_invertible_map(mortar_reference_mapping_tolerance))
1177  return fail("the projected sub-element map is degenerate or non-invertible");
1178 
1179  if (element.type() == QUAD4)
1180  for (const auto corner : make_range(element.n_vertices()))
1181  {
1182  Point tangent_xi;
1183  Point tangent_eta;
1184  for (const auto node : make_range(element.n_nodes()))
1185  {
1186  tangent_xi += FEInterface::shape_deriv(
1187  fe_type, 0, &element, node, 0, element.master_point(corner)) *
1188  element.point(node);
1189  tangent_eta += FEInterface::shape_deriv(
1190  fe_type, 0, &element, node, 1, element.master_point(corner)) *
1191  element.point(node);
1192  }
1193 
1194  const Real corner_jacobian = tangent_xi.cross(tangent_eta).norm();
1195  if (!std::isfinite(corner_jacobian) ||
1196  corner_jacobian <= mortar_reference_mapping_tolerance)
1197  return fail("the projected quadrilateral has a singular or ill-conditioned corner map");
1198  }
1199 
1200  Point local_reference = FEMap::inverse_map(
1201  2, &element, normalized_point, mortar_reference_mapping_tolerance, false, false);
1202  if (!isFinitePoint(local_reference))
1203  return fail("libMesh inverse_map produced a non-finite reference point");
1204 
1205  const Real inverse_map_error =
1206  (FEMap::map(2, &element, local_reference) - normalized_point).norm();
1207  if (!std::isfinite(inverse_map_error) || inverse_map_error > mortar_reference_mapping_tolerance)
1208  {
1209  std::ostringstream reason;
1210  reason << "the normalized inverse-map error " << inverse_map_error << " exceeds "
1211  << mortar_reference_mapping_tolerance;
1212  return fail(reason.str());
1213  }
1214 
1215  if (element.type() == TRI3)
1216  {
1217  std::array<Real, 3> weights;
1218  for (const auto i : index_range(weights))
1219  weights[i] = FEInterface::shape(fe_type, &element, i, local_reference, false);
1220 
1221  for (auto & weight : weights)
1222  weight = std::clamp(weight, 0., 1.);
1223  const Real weight_sum = std::accumulate(weights.begin(), weights.end(), 0.);
1224  if (!std::isfinite(weight_sum) || weight_sum <= singular_tolerance)
1225  return fail("clamped triangle barycentric coordinates have a zero or non-finite sum");
1226  for (auto & weight : weights)
1227  weight /= weight_sum;
1228 
1229  // Preserve partition of unity after clamping tolerance-sized violations.
1230  const auto corrected_weight =
1231  std::distance(weights.begin(), std::max_element(weights.begin(), weights.end()));
1232  weights[corrected_weight] = 1.;
1233  for (const auto i : index_range(weights))
1234  if (i != static_cast<unsigned int>(corrected_weight))
1235  weights[corrected_weight] -= weights[i];
1236 
1237  local_reference = Point();
1238  for (const auto i : index_range(weights))
1239  local_reference += weights[i] * element.master_point(i);
1240  }
1241  else
1242  {
1243  local_reference(0) = std::clamp(local_reference(0), -1., 1.);
1244  local_reference(1) = std::clamp(local_reference(1), -1., 1.);
1245  local_reference(2) = 0.;
1246  }
1247 
1248  if (!element.on_reference_element(local_reference, mortar_reference_mapping_tolerance))
1249  return fail("the clamped inverse-map result is outside the reference element");
1250 
1251  const Real round_trip_error =
1252  (FEMap::map(2, &element, local_reference) - normalized_point).norm();
1253  if (!std::isfinite(round_trip_error) || round_trip_error > reference_tolerance)
1254  {
1255  std::ostringstream reason;
1256  reason << "the normalized inverse-map round-trip error " << round_trip_error
1257  << " exceeds the clipping-consistent tolerance " << reference_tolerance;
1258  return fail(reason.str());
1259  }
1260 
1261  Point parent_reference;
1262  for (const auto i : index_range(normalized_poly))
1263  parent_reference +=
1264  FEInterface::shape(fe_type, &element, i, local_reference, false) * reference_points[i];
1265 
1266  if (!isFinitePoint(parent_reference))
1267  return fail("reference interpolation produced a non-finite parent reference point");
1268 
1269  return parent_reference;
1270  };
1271 
1272  if (poly.size() == 3)
1273  {
1274  Tri3 element;
1275  return recover_with_libmesh(element);
1276  }
1277 
1278  Quad4 element;
1279  return recover_with_libmesh(element);
1280 }
1281 
1282 Real
1283 MortarSegmentHelper::area(const std::vector<Point> & nodes) const
1284 {
1285  Real poly_area = 0;
1286  for (auto i : index_range(nodes))
1287  poly_area += nodes[i](0) * nodes[(i + 1) % nodes.size()](1) -
1288  nodes[i](1) * nodes[(i + 1) % nodes.size()](0);
1289  poly_area *= 0.5;
1290  return poly_area;
1291 }
LAGRANGE
std::vector< Point > clipProjectedPoly(const std::vector< Point > &primary_poly) const
Clip an already projected primary polygon against the secondary polygon.
MetaPhysicL::DualNumber< V, D, asd > abs(const MetaPhysicL::DualNumber< V, D, asd > &a)
Definition: EigenADReal.h:50
std::optional< Point > referencePoint(const Point &point, const std::vector< Point > &poly, const std::vector< Point > &reference_points, std::string *failure_reason=nullptr) const
Recover a parent-reference point from a projected sub-element map.
Point _center
Geometric center of secondary element.
Real _area_tol
Tolerance times secondary area for dimensional consistency.
bool isInsideSecondary(const Point &pt) const
Check that a point is inside the secondary polygon (for verification only)
R poly(const C &c, const T x, const bool derivative=false)
Evaluate a polynomial with the coefficients c at x.
Definition: MathUtils.h:242
auto norm() const
const unsigned int invalid_uint
MortarSegmentHelper(std::vector< Point > secondary_nodes, const Point &center, const Point &normal, const MortarSegmentTriangulationMode triangulation_mode, const bool triangulate_triangles)
Construct a helper that generates mortar segment geometry only.
void getMortarSegments(const std::vector< Point > &primary_nodes, std::vector< Point > &nodes, std::vector< std::vector< unsigned int >> &elem_to_nodes)
Get mortar segments generated by a secondary and primary element pair.
const std::vector< Point > & primary_reference_points
Real _length_tol
Tolerance times secondary area for dimensional consistency.
void mooseError(Args &&... args)
Emit an error message with the given stringified, concatenated args and terminate the application...
Definition: MooseError.h:311
static constexpr Real TOLERANCE
void swap(std::vector< T > &data, const std::size_t idx0, const std::size_t idx1, const libMesh::Parallel::Communicator &comm)
Swap function for serial or distributed vector of data.
Definition: Shuffle.h:495
FIRST
static constexpr std::size_t dim
This is the dimension of all vector and tensor datastructures used in MOOSE.
Definition: Moose.h:165
std::vector< Point > clipPoly(const std::vector< Point > &primary_nodes) const
Clip secondary element (defined in instantiation) against given primary polygon result is a set of 2D...
The following methods are specializations for using the libMesh::Parallel::packed_range_* routines fo...
Real distance(const Point &p)
std::vector< std::array< Point, 3 > > & elem_to_primary_reference_points
std::vector< Point > projectPrimaryPoly(const std::vector< Point > &primary_nodes) const
Project a primary polygon into the helper plane while preserving the clipping orientation.
auto max(const L &left, const R &right)
void triangulatePoly(std::vector< Point > &poly_nodes, std::vector< std::vector< unsigned int >> &tri_map) const
Triangulate a polygon according to the configured mortar-segment triangulation mode.
std::vector< Point > _secondary_poly
List of projected points on the linearized secondary element.
QUAD4
MortarSegmentTriangulationMode
Real _tolerance
Tolerance for intersection and clipping.
Point _u
Vectors orthogonal to normal that span the plane projection will be performed on. ...
Real _secondary_area
Area of projected secondary element.
Real value(unsigned n, unsigned alpha, unsigned beta, Real x)
std::vector< std::array< Point, 3 > > & elem_to_secondary_reference_points
dof_id_type weight(const MeshBase &mesh, const processor_id_type pid)
Point _normal
Unit normal of the plane used to project and clip the linearized secondary subpatch.
bool isDisjoint(const std::vector< Point > &poly) const
Checks whether polygons are disjoint for an easy out.
const Point & center() const
Get center point of secondary element.
const MortarSegmentTriangulationMode _triangulation_mode
Triangulation mode used for clipped polygons.
Real area(const std::vector< Point > &nodes) const
Compute area of polygon.
TypeVector< typename CompareTypes< Real, T2 >::supertype > cross(const TypeVector< T2 > &v) const
void getMortarSegmentsImpl(const std::vector< Point > &primary_nodes, std::vector< Point > &nodes, std::vector< std::vector< unsigned int >> &elem_to_nodes, ReferenceMappingData *reference_mapping)
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
const bool _triangulate_triangles
Whether already-triangular polygons should still be centroid-subdivided.
This class supports defining mortar segment mesh elements in 3D by projecting secondary and primary e...
CTSub CT_OPERATOR_BINARY CTMul CTCompareLess CTCompareGreater CTCompareEqual _arg template * sqrt(_arg)) *_arg.template D< dtag >()) CT_SIMPLE_UNARY_FUNCTION(tanh
auto norm(const T &a)
Point getIntersection(const Point &p1, const Point &p2, const Point &q1, const Point &q2, Real &s) const
Computes the intersection between line segments defined by point pairs (p1,p2) and (q1...
IntRange< T > make_range(T beg, T end)
std::vector< Point > _secondary_reference_points
Parent reference points corresponding to _secondary_poly.
Output containers and filtering data used while generating reference-coordinate mappings.
T clamp(const T &x, T2 lowerlimit, T2 upperlimit)
Definition: MathUtils.h:314
Real _remaining_area_fraction
Fraction of area remaining after overlapping primary polygons clipped.
auto min(const L &left, const R &right)
auto index_range(const T &sizable)
Point point(unsigned int i) const
Get 3D position of node of linearized secondary element.