14#include "libmesh/enum_to_string.h"
15#include "libmesh/fe_interface.h"
16#include "metaphysicl/dualnumberarray.h"
25using MetaPhysicL::NumberArray;
27typedef DualNumber<Real, NumberArray<2, Real>>
Dual2;
36constexpr Real coefficient_tolerance = 1e-14;
37constexpr Real jacobian_tolerance = 1e-12;
39constexpr Real root_tolerance = 1e-10;
40constexpr Real inverse_residual_tolerance = 1e-10;
42constexpr Real mortar_reference_tolerance = 1e-8;
45cross2D(
const Point & first,
const Point & second)
47 return first(0) * second(1) - first(1) * second(0);
66std::vector<unsigned int>
69 if (sub_elem >= parent_elem.n_sub_elem())
70 mooseError(
"Invalid 3D mortar sub-element index ",
72 " for parent element ",
75 libMesh::Utility::enum_to_string<ElemType>(parent_elem.type()),
77 parent_elem.n_sub_elem(),
80 switch (parent_elem.type())
99 mooseError(
"Invalid 3D mortar triangular sub-element index ", sub_elem,
".");
115 mooseError(
"Invalid 3D mortar QUAD8 sub-element index ", sub_elem,
".");
129 mooseError(
"Invalid 3D mortar QUAD9 sub-element index ", sub_elem,
".");
134 " has unsupported type ",
135 libMesh::Utility::enum_to_string<ElemType>(parent_elem.type()),
136 " for 3D mortar sub-element topology.");
143subElementType(
const ElemType parent_type,
const unsigned int sub_elem)
155 return sub_elem == 4 ? QUAD4 : TRI3;
158 libMesh::Utility::enum_to_string<ElemType>(parent_type),
159 " for 3D mortar projection.");
164quadrilateralReferenceViolation(
const Point & point)
166 return std::max({
Real(0), -1 - point(0), point(0) - 1, -1 - point(1), point(1) - 1});
170projectionFailure(
const Elem & msm_elem,
171 const Elem & parent_elem,
172 const unsigned int sub_elem,
173 const unsigned int qp,
174 const std::string & reason)
176 mooseException(
"Unable to map 3D mortar quadrature point ",
178 " from mortar segment ",
182 " of parent element ",
185 libMesh::Utility::enum_to_string<ElemType>(parent_elem.type()),
191realPolynomialRoots(
const Real quadratic,
const Real linear,
const Real constant)
193 PolynomialRoots roots;
194 const Real scale = std::max({std::abs(quadratic), std::abs(linear), std::abs(constant)});
201 if (std::abs(a) <= coefficient_tolerance)
203 if (std::abs(b) <= coefficient_tolerance)
205 roots.values[roots.count++] = -c / b;
209 Real discriminant = b * b - 4 * a * c;
210 const Real discriminant_scale = b * b + std::abs(4 * a * c);
211 if (discriminant < -coefficient_tolerance * std::max(discriminant_scale,
Real(1)))
213 discriminant = std::max(discriminant,
Real(0));
215 const Real sqrt_discriminant = std::sqrt(discriminant);
217 const Real q = -0.5 * (b + std::copysign(sqrt_discriminant, b));
218 if (std::abs(q) <= coefficient_tolerance)
220 roots.values[roots.count++] = -b / (2 * a);
224 const Real first_root = q / a;
225 const Real second_root = c / q;
226 roots.values[roots.count++] = first_root;
227 if (std::abs(first_root - second_root) <= root_tolerance)
229 roots.values[roots.count++] = second_root;
234evaluateBilinear(
const BilinearMap & map,
const Point & target,
const Real
xi,
const Real
eta)
236 return map.center - target +
xi * map.xi +
eta * map.eta +
xi *
eta * map.mixed;
239template <std::
size_t N>
241projectToNormalizedPlane(
const Elem & msm_elem,
242 const Elem & parent_elem,
243 const std::vector<unsigned int> & sub_elem_node_indices,
244 const Point & normal,
245 const Point & target,
246 const unsigned int sub_elem,
247 const unsigned int qp,
248 const char *
const sub_elem_name,
249 std::array<Point, N> & projected_nodes,
250 Point & projected_target)
252 mooseAssert(sub_elem_node_indices.size() == N,
"Unexpected mortar subpatch node count.");
254 Point longest_projected_edge;
255 Real length_scale = 0;
256 for (
const auto first :
index_range(sub_elem_node_indices))
258 const auto second = (first + 1) % sub_elem_node_indices.size();
259 const Point edge = parent_elem.point(sub_elem_node_indices[second]) -
260 parent_elem.point(sub_elem_node_indices[first]);
261 const Point projected_edge = edge - (edge * normal) * normal;
262 if (projected_edge.norm() > length_scale)
264 length_scale = projected_edge.norm();
265 longest_projected_edge = projected_edge;
269 if (!std::isfinite(length_scale) || length_scale == 0)
270 projectionFailure(msm_elem,
274 std::string(
"the projected ") + sub_elem_name +
" is singular");
276 const Point first_tangent = longest_projected_edge / length_scale;
277 const Point second_tangent = normal.cross(first_tangent).unit();
278 const Point origin = parent_elem.point(sub_elem_node_indices[0]);
280 for (
const auto node :
index_range(sub_elem_node_indices))
282 const Point offset = parent_elem.point(sub_elem_node_indices[node]) - origin;
283 projected_nodes[node] =
284 Point((offset * first_tangent) / length_scale, (offset * second_tangent) / length_scale);
286 const Point target_offset = target - origin;
287 projected_target = Point((target_offset * first_tangent) / length_scale,
288 (target_offset * second_tangent) / length_scale);
292analyticalTriangleInverse(
const Elem & msm_elem,
293 const Elem & parent_elem,
294 const std::vector<unsigned int> & sub_elem_node_indices,
295 const Point & normal,
296 const Point & target,
297 const unsigned int sub_elem,
298 const unsigned int qp)
300 std::array<Point, 3> projected_nodes;
301 Point projected_target;
302 projectToNormalizedPlane(msm_elem,
304 sub_elem_node_indices,
313 const Point first_edge = projected_nodes[1] - projected_nodes[0];
314 const Point second_edge = projected_nodes[2] - projected_nodes[0];
315 const Real determinant = cross2D(first_edge, second_edge);
316 if (std::abs(determinant) <= jacobian_tolerance)
317 projectionFailure(msm_elem, parent_elem, sub_elem, qp,
"the projected TRI3 is singular");
319 const Point right_hand_side = projected_target - projected_nodes[0];
320 const Real xi = cross2D(right_hand_side, second_edge) / determinant;
321 const Real eta = cross2D(first_edge, right_hand_side) / determinant;
322 if (!std::isfinite(
xi) || !std::isfinite(
eta))
323 projectionFailure(msm_elem, parent_elem, sub_elem, qp,
"the TRI3 inverse is not finite");
325 const Point unsnapped_result(
xi,
eta);
326 const Point unsnapped_residual =
327 projected_nodes[0] +
xi * first_edge +
eta * second_edge - projected_target;
328 if (unsnapped_residual.norm() > inverse_residual_tolerance)
330 msm_elem, parent_elem, sub_elem, qp,
"the TRI3 inverse does not satisfy the projection");
332 std::array<Real, 3> barycentric = {{1 -
xi -
eta,
xi,
eta}};
333 const Real violation = std::max({
Real(0), -barycentric[0], -barycentric[1], -barycentric[2]});
335 return unsnapped_result;
336 if (violation > mortar_reference_tolerance)
338 msm_elem, parent_elem, sub_elem, qp,
"the TRI3 inverse is outside the subpatch");
342 for (
auto & coordinate : barycentric)
344 const Real barycentric_sum = barycentric[0] + barycentric[1] + barycentric[2];
345 for (
auto & coordinate : barycentric)
346 coordinate /= barycentric_sum;
348 const Point result(barycentric[1], barycentric[2]);
349 const Point snapped_residual =
350 projected_nodes[0] + result(0) * first_edge + result(1) * second_edge - projected_target;
351 if (snapped_residual.norm() > mortar_reference_tolerance)
352 projectionFailure(msm_elem,
356 "the snapped TRI3 inverse does not satisfy the projection");
362prepareQuadrilateralMap(
const std::array<Point, 4> & points,
363 const Elem & msm_elem,
364 const Elem & parent_elem,
365 const unsigned int sub_elem,
366 const unsigned int qp)
369 map.center = 0.25 * (points[0] + points[1] + points[2] + points[3]);
370 map.xi = 0.25 * (-points[0] + points[1] + points[2] - points[3]);
371 map.eta = 0.25 * (-points[0] - points[1] + points[2] + points[3]);
372 map.mixed = 0.25 * (points[0] - points[1] + points[2] - points[3]);
373 map.scale = std::max({map.xi.norm(), map.eta.norm(), map.mixed.norm()});
374 if (!std::isfinite(map.scale) || map.scale == 0)
376 msm_elem, parent_elem, sub_elem, qp,
"the projected QUAD4 has invalid coefficients");
379 Real orientation = 0;
380 for (
const auto xi : {-1.0, 1.0})
381 for (
const auto eta : {-1.0, 1.0})
383 const Real determinant = cross2D(map.xi +
eta * map.mixed, map.eta +
xi * map.mixed);
384 if (std::abs(determinant) <= jacobian_tolerance)
385 projectionFailure(msm_elem, parent_elem, sub_elem, qp,
"the projected QUAD4 is singular");
386 if (orientation == 0)
387 orientation = std::copysign(1.0, determinant);
388 else if (orientation * determinant < 0)
389 projectionFailure(msm_elem, parent_elem, sub_elem, qp,
"the projected QUAD4 is folded");
395inverseMapQuadrilateral(
const BilinearMap & map,
396 const Point & target,
397 const Elem & msm_elem,
398 const Elem & parent_elem,
399 const unsigned int sub_elem,
400 const unsigned int qp)
402 std::vector<Point> strict_candidates;
403 std::vector<Point> tolerance_candidates;
405 auto store_candidate = [](
const Point & candidate,
auto & candidates)
407 if (std::none_of(candidates.begin(),
409 [&candidate](
const Point & existing)
410 { return (candidate - existing).norm() <= root_tolerance; }))
411 candidates.push_back(candidate);
416 Point candidate(
xi,
eta);
420 if (evaluateBilinear(map, target,
xi,
eta).
norm() > inverse_residual_tolerance)
423 const Real violation = quadrilateralReferenceViolation(candidate);
426 store_candidate(candidate, strict_candidates);
429 if (violation > mortar_reference_tolerance)
434 const Point unsnapped_candidate = candidate;
435 candidate(0) = std::clamp(candidate(0),
Real(-1),
Real(1));
436 candidate(1) = std::clamp(candidate(1),
Real(-1),
Real(1));
437 if (evaluateBilinear(map, target, candidate(0), candidate(1)).
norm() >
438 mortar_reference_tolerance)
440 store_candidate(unsnapped_candidate, tolerance_candidates);
444 const auto xi_roots =
445 realPolynomialRoots(cross2D(map.xi, map.mixed),
446 cross2D(map.center - target, map.mixed) + cross2D(map.xi, map.eta),
447 cross2D(map.center - target, map.eta));
448 const Real direction_tolerance_sq =
449 coefficient_tolerance * coefficient_tolerance * map.scale * map.scale;
452 const Real xi = xi_roots.values[root];
453 const Point eta_direction = map.eta +
xi * map.mixed;
454 const Real denominator = eta_direction.norm_sq();
455 if (denominator > direction_tolerance_sq)
456 add_candidate(
xi, (((target - map.center) -
xi * map.xi) * eta_direction) / denominator);
459 const auto eta_roots =
460 realPolynomialRoots(cross2D(map.eta, map.mixed),
461 cross2D(map.center - target, map.mixed) + cross2D(map.eta, map.xi),
462 cross2D(map.center - target, map.xi));
465 const Real eta = eta_roots.values[root];
466 const Point xi_direction = map.xi +
eta * map.mixed;
467 const Real denominator = xi_direction.norm_sq();
468 if (denominator > direction_tolerance_sq)
469 add_candidate((((target - map.center) -
eta * map.eta) * xi_direction) / denominator,
eta);
473 if (strict_candidates.size() == 1)
474 return strict_candidates[0];
475 if (strict_candidates.empty() && tolerance_candidates.size() == 1)
477 auto candidate = tolerance_candidates[0];
478 candidate(0) = std::clamp(candidate(0),
Real(-1),
Real(1));
479 candidate(1) = std::clamp(candidate(1),
Real(-1),
Real(1));
483 projectionFailure(msm_elem,
487 "the analytical fallback did not find one unique in-domain QUAD4 inverse");
491analyticalQuadrilateralInverse(
const Elem & msm_elem,
492 const Elem & parent_elem,
493 const std::vector<unsigned int> & sub_elem_node_indices,
494 const Point & normal,
495 const Point & target,
496 const unsigned int sub_elem,
497 const unsigned int qp)
499 std::array<Point, 4> projected_nodes;
500 Point projected_target;
501 projectToNormalizedPlane(msm_elem,
503 sub_elem_node_indices,
512 return inverseMapQuadrilateral(
513 prepareQuadrilateralMap(projected_nodes, msm_elem, parent_elem, sub_elem, qp),
525 const QBase & qrule_msm,
526 std::vector<Point> & secondary_q_pts,
527 std::vector<Point> & primary_q_pts)
529 mooseAssert(mortar_segment_elem.type() == TRI3,
530 "Reference interpolation expects triangular mortar segments.");
531 const FEType fe_type(FIRST, LAGRANGE);
533 for (
const auto qp : make_range(qrule_msm.n_points()))
541 FEInterface::shape(fe_type, &mortar_segment_elem, n, qrule_msm.qp(qp),
false);
546 secondary_q_pts.push_back(secondary_qp);
547 primary_q_pts.push_back(primary_qp);
553 const Elem *
const primal_elem,
554 const unsigned int sub_elem_index,
555 const QBase & qrule_msm,
556 std::vector<Point> & q_pts)
558 const auto msm_elem_order = msm_elem->default_order();
559 const auto msm_elem_type = msm_elem->type();
562 const Point e1 = msm_elem->point(0) - msm_elem->point(1);
563 const Point e2 = msm_elem->point(2) - msm_elem->point(1);
564 const Point normal = e2.cross(e1).unit();
567 const auto sub_elem = msm_elem->get_extra_integer(sub_elem_index);
568 const ElemType primal_type = primal_elem->type();
569 const ElemType sub_elem_type = subElementType(primal_type, sub_elem);
573 auto transform_qp = [primal_type, sub_elem](
const Real nu,
const Real
xi)
578 return Point(nu,
xi, 0);
580 return Point(nu,
xi, 0);
586 return Point(0.5 * nu, 0.5 *
xi, 0);
588 return Point(0.5 * (1 -
xi), 0.5 * (nu +
xi), 0);
590 return Point(0.5 * (1 + nu), 0.5 *
xi, 0);
592 return Point(0.5 * nu, 0.5 * (1 +
xi), 0);
594 mooseError(
"get_sub_elem_indices: Invalid sub_elem: ", sub_elem);
600 return Point(nu - 1,
xi - 1, 0);
602 return Point(nu +
xi,
xi - 1, 0);
604 return Point(1 -
xi, nu +
xi, 0);
606 return Point(nu - 1, nu +
xi, 0);
608 return Point(0.5 * (nu -
xi), 0.5 * (nu +
xi), 0);
610 mooseError(
"get_sub_elem_indices: Invalid sub_elem: ", sub_elem);
616 return Point(0.5 * (nu - 1), 0.5 * (
xi - 1), 0);
618 return Point(0.5 * (nu + 1), 0.5 * (
xi - 1), 0);
620 return Point(0.5 * (nu + 1), 0.5 * (
xi + 1), 0);
622 return Point(0.5 * (nu - 1), 0.5 * (
xi + 1), 0);
624 mooseError(
"get_sub_elem_indices: Invalid sub_elem: ", sub_elem);
627 mooseError(
"transform_qp: Face element type: ",
628 libMesh::Utility::enum_to_string<ElemType>(primal_type),
629 " invalid for 3D mortar");
637 for (
auto qp : make_range(qrule_msm.n_points()))
641 for (
auto n : make_range(msm_elem->n_nodes()))
645 static_cast<const TypeVector<Real> &
>(qrule_msm.qp(qp))) *
648 if (sub_elem_type == TRI3)
650 const Point sub_elem_point = analyticalTriangleInverse(
651 *msm_elem, *primal_elem, sub_elem_node_indices, normal, x0, sub_elem, qp);
652 const Point parent_point = transform_qp(sub_elem_point(0), sub_elem_point(1));
654 !primal_elem->on_reference_element(parent_point, mortar_reference_tolerance))
655 projectionFailure(*msm_elem,
659 "the recovered TRI3 point is outside the parent face");
660 q_pts.push_back(parent_point);
667 xi1.value() = qrule_msm.qp(qp)(0);
668 xi1.derivatives()[0] = 1.0;
670 xi2.value() = qrule_msm.qp(qp)(1);
671 xi2.derivatives()[1] = 1.0;
672 VectorValue<Dual2>
xi(xi1, xi2, 0);
673 unsigned int current_iterate = 0, max_iterates = 10;
679 VectorValue<Dual2> x1;
680 for (
auto n : make_range(sub_elem_node_indices.size()))
682 primal_elem->point(sub_elem_node_indices[n]);
685 VectorValue<Dual2> F(u(1) * normal(2) - u(2) * normal(1),
686 u(2) * normal(0) - u(0) * normal(2),
687 u(0) * normal(1) - u(1) * normal(0));
689 Real projection_tolerance(1e-10);
695 if (!u.is_zero() && u.norm().value() > 1.0)
696 projection_tolerance *= u.norm().value();
701 RealEigenMatrix J(3, 2);
702 J << F(0).derivatives()[0], F(0).derivatives()[1], F(1).derivatives()[0],
703 F(1).derivatives()[1], F(2).derivatives()[0], F(2).derivatives()[1];
704 RealEigenVector f(3);
705 f << F(0).value(), F(1).value(), F(2).value();
706 const RealEigenVector dxi = -J.colPivHouseholderQr().solve(f);
710 }
while (++current_iterate < max_iterates);
712 const Point newton_sub_elem_point(
xi(0).value(),
xi(1).value());
713 const Point newton_parent_point =
714 transform_qp(newton_sub_elem_point(0), newton_sub_elem_point(1));
715 const bool newton_point_is_valid =
718 quadrilateralReferenceViolation(newton_sub_elem_point) == 0 &&
719 primal_elem->on_reference_element(newton_parent_point, mortar_reference_tolerance);
721 if (newton_point_is_valid)
723 q_pts.push_back(newton_parent_point);
727 if (sub_elem_type == QUAD4)
730 const Point fallback_point = analyticalQuadrilateralInverse(
731 *msm_elem, *primal_elem, sub_elem_node_indices, normal, x0, sub_elem, qp);
732 const Point parent_point = transform_qp(fallback_point(0), fallback_point(1));
734 !primal_elem->on_reference_element(parent_point, mortar_reference_tolerance))
735 projectionFailure(*msm_elem,
739 "the recovered point is outside the parent face");
740 q_pts.push_back(parent_point);
744 if (current_iterate == max_iterates)
745 mooseError(
"Newton iteration for mortar quadrature mapping msm element: ",
749 " didn't converge. MSM element volume: ",
753 *msm_elem, *primal_elem, sub_elem, qp,
"the Newton result is outside the parent face");
void mooseError(Args &&... args)
Emit an error message with the given stringified, concatenated args and terminate the application.
DualNumber< Real, NumberArray< 2, Real > > Dual2
std::array< Real, 2 > values
T clamp(const T &x, T2 lowerlimit, T2 upperlimit)
bool isFinitePoint(const Point &point)
void projectQPoints3d(const Elem *msm_elem, const Elem *primal_elem, unsigned int sub_elem_index, const QBase &qrule_msm, std::vector< Point > &q_pts)
3D projection operator for mapping qpoints on mortar segments to secondary or primary elements
std::vector< unsigned int > getMortarSubElementNodeIndices(const Elem &parent_elem, unsigned int sub_elem)
Return the node indices for a first-order sub-element of a parent face.
void mapQPoints3dFromReference(const Elem &mortar_segment_elem, const MortarSegmentReferencePoints &reference_points, const QBase &qrule_msm, std::vector< Point > &secondary_q_pts, std::vector< Point > &primary_q_pts)
3D mapping operator that interpolates stored parent reference points on each triangular mortar segmen...
MOOSE now contains C++17 code, so give a reasonable error message stating what the user can do to add...
T fe_lagrange_2D_shape(const libMesh::ElemType type, const Order order, const unsigned int i, const VectorType< T > &p)
auto index_range(const T &sizable)
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
IntRange< T > make_range(T beg, T end)
Parent-face reference coordinates associated with the vertices of one triangular mortar segment.
std::array< Point, 3 > primary_reference_points
std::array< Point, 3 > secondary_reference_points