LCOV - code coverage report
Current view: top level - src/systems - variational_smoother_constraint.C (source / functions) Hit Total Coverage
Test: libMesh/libmesh: #4640 (c5f9f0) with base a68dd6 Lines: 344 384 89.6 %
Date: 2026-11-02 23:02:27 Functions: 50 62 80.6 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : // The libMesh Finite Element Library.
       2             : // Copyright (C) 2002-2026 Benjamin S. Kirk, John W. Peterson, Roy H. Stogner
       3             : 
       4             : // This library is free software; you can redistribute it and/or
       5             : // modify it under the terms of the GNU Lesser General Public
       6             : // License as published by the Free Software Foundation; either
       7             : // version 2.1 of the License, or (at your option) any later version.
       8             : 
       9             : // This library is distributed in the hope that it will be useful,
      10             : // but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             : // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
      12             : // Lesser General Public License for more details.
      13             : 
      14             : // You should have received a copy of the GNU Lesser General Public
      15             : // License along with this library; if not, write to the Free Software
      16             : // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
      17             : 
      18             : // Local Includes
      19             : #include "libmesh/variational_smoother_constraint.h"
      20             : #include "libmesh/mesh_tools.h"
      21             : #include "libmesh/boundary_info.h"
      22             : 
      23             : namespace libMesh
      24             : {
      25             : 
      26             : // Helper function to orient a unit vector in the positive x/y/z direction
      27      795061 : auto get_positive_vector = [](const Point & vec) -> Point {
      28             :   // unit() rounds to within a few ulps of length 1, so TOLERANCE only
      29             :   // rejects vectors that were never normalized
      30       66021 :   libmesh_assert(absolute_fuzzy_equals(vec.norm(), 1., TOLERANCE));
      31             : 
      32             :   // Choose sign such that direction vector points in the positive x/y/z
      33             :   // direction This helps to eliminate duplicate lines/planes
      34       66021 :   Point canonical{0, 0, 0};
      35             :   // Choose the canonical dimension to ensure the dot product below is nonzero
      36     1542106 :   for (const auto dim_id : make_range(3))
      37     1670170 :     if (!absolute_fuzzy_equals(vec(dim_id), 0.))
      38             :       {
      39      795061 :         canonical(dim_id) = 1.;
      40      729040 :         break;
      41             :       }
      42             : 
      43       66021 :   const auto dot_prod = vec * canonical;
      44       66021 :   libmesh_assert(!absolute_fuzzy_equals(dot_prod, 0.));
      45             : 
      46      795061 :   return (dot_prod > 0) ? vec : Point(-vec);
      47             : };
      48             : 
      49     1469398 : PointConstraint::PointConstraint(const Point & point, const Real & tol) : _point(point), _tol(tol)
      50             : {
      51     1469398 : }
      52             : 
      53           0 : bool PointConstraint::operator<(const PointConstraint & other) const
      54             : {
      55           0 :   if (*this == other)
      56           0 :     return false;
      57             : 
      58           0 :   return _point < other.point();
      59             : }
      60             : 
      61         192 : bool PointConstraint::operator==(const PointConstraint & other) const
      62             : {
      63         192 :   return _point.absolute_fuzzy_equals(other.point(), _tol);
      64             : }
      65             : 
      66       13868 : ConstraintVariant PointConstraint::intersect(const ConstraintVariant & other) const
      67             : {
      68             :   // using visit to resolve the variant to its actual type
      69             :   return std::visit(
      70       37040 :       [&](auto && o) -> ConstraintVariant {
      71       13868 :         if (!o.contains_point(*this))
      72             :           // Point is not on the constraint
      73           0 :           return InvalidConstraint();
      74             : 
      75        1141 :         return *this;
      76             :       },
      77       13868 :       other);
      78             : }
      79             : 
      80       13270 : LineConstraint::LineConstraint(const Point & point, const Point & direction, const Real & tol)
      81       13270 :   : _point(point), _direction(get_positive_vector(direction)), _tol(tol)
      82             : {
      83        1098 :   libmesh_assert_msg(_direction.norm() >= _tol,
      84             :                      "Can't define a line with zero magnitude direction vector.");
      85       13270 : }
      86             : 
      87           0 : bool LineConstraint::operator<(const LineConstraint & other) const
      88             : {
      89           0 :   if (*this == other)
      90           0 :     return false;
      91             : 
      92           0 :   if (!(_direction.absolute_fuzzy_equals(other.direction(), _tol)))
      93           0 :     return _direction < other.direction();
      94           0 :   return (_direction * _point) < (other.direction() * other.point());
      95             : }
      96             : 
      97          48 : bool LineConstraint::operator==(const LineConstraint & other) const
      98             : {
      99          52 :   if (!(_direction.absolute_fuzzy_equals(other.direction(), _tol)))
     100           4 :     return false;
     101           0 :   return this->contains_point(other.point());
     102             : }
     103             : 
     104         599 : bool LineConstraint::contains_point(const PointConstraint & p) const
     105             : {
     106             :   // If the point lies on the line, then the vector p - point is parallel to the
     107             :   // line In that case, the cross product of p - point with the line's direction
     108             :   // will be zero.
     109         599 :   return _direction.cross(p.point() - _point).norm() < _tol;
     110             : }
     111             : 
     112          48 : bool LineConstraint::is_parallel(const LineConstraint & l) const
     113             : {
     114          48 :   return _direction.absolute_fuzzy_equals(l.direction(), _tol);
     115             : }
     116             : 
     117        3453 : bool LineConstraint::is_parallel(const PlaneConstraint & p) const
     118             : {
     119        3453 :   return std::abs(_direction * p.normal()) < _tol;
     120             : }
     121             : 
     122        4139 : ConstraintVariant LineConstraint::intersect(const ConstraintVariant & other) const
     123             : {
     124             :   // using visit to resolve the variant to its actual type
     125             :   return std::visit(
     126       11053 :       [&](auto && o) -> ConstraintVariant {
     127             :         // Use std::decay_t to strip references/const/volatile from o's type,
     128             :         // so we can match the actual stored variant type in std::is_same_v.
     129             :         using T = std::decay_t<decltype(o)>;
     130             :         if constexpr (std::is_same_v<T, LineConstraint>)
     131             :           {
     132        4183 :             if (*this == o)
     133           0 :               return *this;
     134             : 
     135          48 :             if (this->is_parallel(o))
     136             :               // Lines are parallel and do not intersect
     137           0 :               return InvalidConstraint();
     138             : 
     139             :             // Solve for t in the equation p1 + t·d1 = p2 + s·d2
     140             :             // The shortest vector between skew lines lies along the normal vector
     141             :             // (d1 × d2). Projecting the vector (p2 - p1) onto this normal gives a
     142             :             // scalar proportional to the distance. This is equivalent to solving:
     143             :             //   ((p2 - p1) × d2) · (d1 × d2) = t · |d1 × d2|²
     144             :             //   ⇒ t = ((delta × d2) · (d1 × d2)) / |d1 × d2|²
     145             : 
     146           4 :             const Point delta = o.point() - _point;
     147          44 :             const Point cross_d1_d2 = _direction.cross(o.direction());
     148          44 :             const Real cross_dot = (delta.cross(o.direction())) * cross_d1_d2;
     149           4 :             const Real denom = cross_d1_d2.norm_sq();
     150             : 
     151          48 :             const Real t = cross_dot / denom;
     152           4 :             const Point intersection = _point + t * _direction;
     153             : 
     154             :             // Verify that intersection lies on both lines
     155          48 :             if (o.direction().cross(intersection - o.point()).norm() > _tol)
     156             :               // Lines do not intersect at a single point
     157           0 :               return InvalidConstraint();
     158             : 
     159          48 :             return PointConstraint{intersection};
     160             :           }
     161             : 
     162             :         else if constexpr (std::is_same_v<T, PlaneConstraint>)
     163        7845 :           return o.intersect(*this);
     164             : 
     165             :         else if constexpr (std::is_same_v<T, PointConstraint>)
     166             :           {
     167           0 :             if (!this->contains_point(o))
     168             :               // Point is not on the line
     169           0 :               return InvalidConstraint();
     170             : 
     171           0 :             return o;
     172             :           }
     173             : 
     174             :         else
     175           0 :           libmesh_error_msg("Unsupported constraint type in Line::intersect.");
     176             :       },
     177        4139 :       other);
     178             : }
     179             : 
     180      781791 : PlaneConstraint::PlaneConstraint(const Point & point, const Point & normal, const Real & tol)
     181      781791 :   : _point(point), _normal(get_positive_vector(normal)), _tol(tol)
     182             : {
     183       64923 :   libmesh_assert_msg(_normal.norm() >= _tol,
     184             :                      "Can't define a plane with zero magnitude direction vector.");
     185      781791 : }
     186             : 
     187     1817863 : bool PlaneConstraint::operator<(const PlaneConstraint & other) const
     188             : {
     189     1817863 :   if (*this == other)
     190      121302 :     return false;
     191             : 
     192      386796 :   if (!(_normal.absolute_fuzzy_equals(other.normal(), _tol)))
     193      357424 :     return _normal < other.normal();
     194           0 :   return (_normal * _point) < (other.normal() * other.point());
     195             : }
     196             : 
     197     1828709 : bool PlaneConstraint::operator==(const PlaneConstraint & other) const
     198             : {
     199     1980279 :   if (!(_normal.absolute_fuzzy_equals(other.normal(), _tol)))
     200       30379 :     return false;
     201     1460439 :   return this->contains_point(other.point());
     202             : }
     203             : 
     204       10846 : bool PlaneConstraint::is_parallel(const PlaneConstraint & p) const
     205             : {
     206             :   // |n1 x n2| is the sine of the angle between the unit normals, and its
     207             :   // square is the determinant that intersect() divides by, so any pair of
     208             :   // planes that fails this test can be intersected.
     209       10846 :   return _normal.cross(p.normal()).norm() <= _tol;
     210             : }
     211             : 
     212        3382 : bool PlaneConstraint::is_parallel(const LineConstraint & l) const { return l.is_parallel(*this); }
     213             : 
     214     1477893 : bool PlaneConstraint::contains_point(const PointConstraint & p) const
     215             : {
     216             :   // distance between the point and the plane
     217      122734 :   const Real dist = (p.point() - _point) * _normal;
     218     1477893 :   return std::abs(dist) < _tol;
     219             : }
     220             : 
     221        4306 : bool PlaneConstraint::contains_line(const LineConstraint & l) const
     222             : {
     223        4306 :   const bool base_on_plane = this->contains_point(PointConstraint(l.point()));
     224        4306 :   const bool dir_orthogonal = std::abs(_normal * l.direction()) < _tol;
     225        4306 :   return base_on_plane && dir_orthogonal;
     226             : }
     227             : 
     228       15081 : ConstraintVariant PlaneConstraint::intersect(const ConstraintVariant & other) const
     229             : {
     230             :   // using visit to resolve the variant to its actual type
     231             :   return std::visit(
     232       40263 :       [&](auto && o) -> ConstraintVariant {
     233             :         // Use std::decay_t to strip references/const/volatile from o's type,
     234             :         // so we can match the actual stored variant type in std::is_same_v.
     235             :         using T = std::decay_t<decltype(o)>;
     236             :         if constexpr (std::is_same_v<T, PlaneConstraint>)
     237             :           {
     238             :             // If planes are identical, return one of them
     239       16498 :             if (*this == o)
     240           0 :               return *this;
     241             : 
     242       10775 :             if (this->is_parallel(o))
     243             :               // Planes are parallel and do not intersect
     244           0 :               return InvalidConstraint();
     245             : 
     246             :             // Solve for a point on the intersection line of two planes.
     247             :             // Given planes:
     248             :             //   Plane 1: n1 · (x - p1) = 0
     249             :             //   Plane 2: n2 · (x - p2) = 0
     250             :             // The line of intersection has direction dir = n1 × n2.
     251             :             // To find a point on this line, we assume:
     252             :             //   x = p1 + s·n1 = p2 + t·n2
     253             :             //   ⇒ p1 - p2 = t·n2 - s·n1
     254             :             // Taking dot products with n1 and n2 leads to:
     255             :             //   [-n1·n1   n1·n2] [s] = [n1 · (p1 - p2)]
     256             :             //   [-n1·n2   n2·n2] [t]   [n2 · (p1 - p2)]
     257             : 
     258        9881 :             const Point dir = this->_normal.cross(o.normal()); // direction of line of intersection
     259         894 :             libmesh_assert(dir.norm() > _tol);
     260         894 :             const Point w = _point - o.point();
     261             : 
     262             :             // Dot product terms used in 2x2 system
     263         894 :             const Real n1_dot_n2 = _normal * o.normal();
     264         894 :             const Real n2_dot_n2 = o.normal() * o.normal();
     265         894 :             const Real n1_dot_w = _normal * w;
     266         894 :             const Real n2_dot_w = o.normal() * w;
     267             : 
     268             :             // By Lagrange's identity the determinant
     269             :             // -(n1.n1 * n2.n2 - (n1.n2)^2) equals -|n1 x n2|^2, which is
     270             :             // nonzero because the planes are not parallel. Computing it from
     271             :             // dir avoids the cancellation in 1 - cos^2 for nearly parallel
     272             :             // planes.
     273         894 :             const Real denom = -dir.norm_sq();
     274             : 
     275       10775 :             const Real s = -(n1_dot_n2 * n2_dot_w - n2_dot_n2 * n1_dot_w) / denom;
     276         894 :             const Point p0 = _point + s * _normal;
     277             : 
     278       10775 :             return LineConstraint{p0, dir.unit()};
     279             :           }
     280             : 
     281             :         else if constexpr (std::is_same_v<T, LineConstraint>)
     282             :           {
     283        4306 :             if (this->contains_line(o))
     284          77 :               return o;
     285             : 
     286        3382 :             if (this->is_parallel(o))
     287             :               // Line is parallel and does not intersect the plane
     288           0 :               return InvalidConstraint();
     289             : 
     290             :             // Solve for t in the parametric equation:
     291             :             //   p(t) = point + t·d
     292             :             // such that this point also satisfies the plane equation:
     293             :             //   n · (p(t) - p0) = 0
     294             :             // which leads to:
     295             :             //   t = (n · (p0 - point)) / (n · d)
     296             : 
     297         274 :             const Real denom = _normal * o.direction();
     298         274 :             libmesh_assert(std::abs(denom) > _tol);
     299        3382 :             const Real t = (_normal * (_point - o.point())) / denom;
     300        3382 :             return PointConstraint{o.point() + t * o.direction()};
     301             :           }
     302             : 
     303             :         else if constexpr (std::is_same_v<T, PointConstraint>)
     304             :           {
     305           0 :             if (!this->contains_point(o))
     306             :               // Point is not on the plane
     307           0 :               return InvalidConstraint();
     308             : 
     309           0 :             return o;
     310             :           }
     311             : 
     312             :         else
     313           0 :           libmesh_error_msg("Unsupported constraint type in Plane::intersect.");
     314             :       },
     315       15081 :       other);
     316             : }
     317             : 
     318        2520 : std::ostream &operator<<(std::ostream &os, const ConstraintVariant & c)
     319             : {
     320        2520 :   if (std::holds_alternative<PointConstraint>(c))
     321         264 :     os << "point constraint: point=" << std::get<PointConstraint>(c).point();
     322             : 
     323        2376 :   else if (std::holds_alternative<LineConstraint>(c))
     324             :     {
     325          48 :       const auto & line = std::get<LineConstraint>(c);
     326         624 :       os << "line constraint: point=" << line.point() << ", direction=" << line.direction();
     327             :     }
     328             : 
     329        1800 :   else if (std::holds_alternative<PlaneConstraint>(c))
     330             :     {
     331         150 :       const auto & plane = std::get<PlaneConstraint>(c);
     332        1950 :       os << "plane constraint: point=" << plane.point() << ", normal=" << plane.normal();
     333             :     }
     334             : 
     335           0 :   else if (std::holds_alternative<InvalidConstraint>(c))
     336           0 :     os << "invalid constraint";
     337             : 
     338        2520 :   return os;
     339             : }
     340             : 
     341        2556 : VariationalSmootherConstraint::VariationalSmootherConstraint(
     342        2556 :     System & sys, const bool & preserve_subdomain_boundaries, const unsigned int verbosity)
     343        2556 :   : Constraint(), _verbosity(verbosity), _sys(sys), _preserve_subdomain_boundaries(preserve_subdomain_boundaries) {
     344        2556 : }
     345             : 
     346        4824 : VariationalSmootherConstraint::~VariationalSmootherConstraint() = default;
     347             : 
     348        2556 : void VariationalSmootherConstraint::constrain()
     349             : {
     350        2556 :   const auto & mesh = _sys.get_mesh();
     351        2556 :   const auto dim = mesh.mesh_dimension();
     352         144 :   const auto & proc_id = mesh.processor_id();
     353             : 
     354             :   // Only compute the node to elem map once
     355         144 :   std::unordered_map<dof_id_type, std::vector<const Elem *>> nodes_to_elem_map;
     356        2556 :   MeshTools::build_nodes_to_elem_map(mesh, nodes_to_elem_map);
     357             : 
     358          72 :   const auto & boundary_info = mesh.get_boundary_info();
     359             : 
     360        2628 :   const auto boundary_node_ids = MeshTools::find_boundary_nodes(mesh);
     361             : 
     362             :   // Identify/constrain subdomain boundary nodes, if requested
     363         144 :   std::unordered_map<dof_id_type, ConstraintVariant> subdomain_boundary_map;
     364        2556 :   if (_preserve_subdomain_boundaries)
     365             :     {
     366      260477 :       for (const auto * elem : mesh.active_element_ptr_range())
     367             :         {
     368       10896 :           const auto & sub_id1 = elem->subdomain_id();
     369      490089 :           for (const auto side : elem->side_index_range())
     370             :             {
     371      397586 :               const auto * neighbor = elem->neighbor_ptr(side);
     372      397586 :               if (neighbor == nullptr)
     373       42510 :                 continue;
     374             : 
     375       41360 :               const auto & sub_id2 = neighbor->subdomain_id();
     376      352520 :               if (sub_id1 == sub_id2)
     377      323386 :                 continue;
     378             : 
     379             :               // elem and neighbor are in different subdomains, and share nodes
     380             :               // that need to be constrained
     381       47282 :               for (const auto local_node_id : elem->nodes_on_side(side))
     382             :                 {
     383       39564 :                   const auto & node = mesh.node_ref(elem->node_id(local_node_id));
     384             :                   // Make sure we haven't already processed this node.
     385             :                   // We leave the responsibility of computing the constraint to
     386             :                   // the proc that owns the node.
     387       39094 :                   if (subdomain_boundary_map.count(node.id()) ||
     388        1748 :                       node.processor_id() != proc_id)
     389       35796 :                     continue;
     390             : 
     391             :                   // Get the relevant nodal neighbors for the subdomain constraint
     392             :                   const auto side_grouped_boundary_neighbors =
     393             :                       get_neighbors_for_subdomain_constraint(
     394        2626 :                           mesh, node, sub_id1, nodes_to_elem_map);
     395             : 
     396             :                   // Determine which constraint should be imposed
     397             :                   const auto subdomain_constraint =
     398        2424 :                       determine_constraint(node, dim, side_grouped_boundary_neighbors);
     399             : 
     400             :                   // This subdomain boundary node does not lie on an external boundary,
     401             :                   // go ahead and impose constraint
     402        2626 :                   if (boundary_node_ids.find(node.id()) == boundary_node_ids.end())
     403             :                     {
     404         912 :                       if (_verbosity > 20)
     405           0 :                         libMesh::out << "Imposing subdomain constraint on "
     406           0 :                                      << std::endl << "  " << node << std::endl
     407           0 :                                      << "    " << subdomain_constraint << std::endl;
     408             : 
     409         912 :                       this->impose_constraint(node, subdomain_constraint);
     410             :                     }
     411             : 
     412             :                   // This subdomain boundary node could lie on an external boundary, save it
     413             :                   // for later to combine with the external boundary constraint.
     414             :                   // We also save constraints for non-boundary nodes so we don't try to
     415             :                   // re-constrain the node when accessed from the neighboring elem.
     416             :                   // See subdomain_boundary_map.count call above.
     417        4646 :                   subdomain_boundary_map[node.id()] = subdomain_constraint;
     418             : 
     419             :                 } // for local_node_id
     420             : 
     421             :             } // for side
     422        2278 :         } // for elem
     423             :     }
     424             : 
     425             :   // Loop through boundary nodes and impose constraints
     426      104020 :   for (const auto & bid : boundary_node_ids)
     427             :     {
     428      101464 :       const auto & node = mesh.node_ref(bid);
     429             :       // We leave the responsibility of computing the constraint to the proc
     430             :       // that owns the node.
     431      101464 :       if (node.processor_id() != proc_id)
     432       76216 :         continue;
     433             : 
     434             :       // Get the relevant nodal neighbors for the boundary constraint
     435             :       const auto side_grouped_boundary_neighbors = get_neighbors_for_boundary_constraint(
     436       27352 :           mesh, node, boundary_node_ids, boundary_info, nodes_to_elem_map);
     437             : 
     438             :       // Determine which constraint should be imposed
     439             :       const auto boundary_constraint =
     440       27352 :           determine_constraint(node, dim, side_grouped_boundary_neighbors);
     441             : 
     442             :       // Check for the case where this boundary node is also part of a subdomain id boundary
     443       25248 :       if (const auto it = subdomain_boundary_map.find(bid); it != subdomain_boundary_map.end())
     444             :         {
     445        1512 :           const auto & subdomain_constraint = it->second;
     446             :           // Combine current boundary constraint with previously determined
     447             :           // subdomain_constraint
     448         252 :           auto combined_constraint = intersect_constraints(subdomain_constraint, boundary_constraint);
     449             : 
     450             :           // This will catch cases where constraints have no intersection
     451             :           // Fall back to fixed node constraint
     452        1512 :           if (std::holds_alternative<InvalidConstraint>(combined_constraint))
     453           0 :             combined_constraint = PointConstraint(node);
     454             : 
     455        1512 :           if (_verbosity > 20)
     456           0 :             libMesh::out << "Imposing boundary/subdomain constraint on "
     457           0 :                          << std::endl << "  " << node << std::endl
     458           0 :                          << "    " << combined_constraint << std::endl;
     459             : 
     460        1512 :           this->impose_constraint(node, combined_constraint);
     461             :         }
     462             : 
     463             :       else
     464             :         {
     465       23736 :           if (_verbosity > 20)
     466         210 :             libMesh::out << "Imposing boundary constraint on "
     467         210 :                          << std::endl << node << std::endl
     468         210 :                          << "    " << boundary_constraint << std::endl;
     469             : 
     470       23736 :           this->impose_constraint(node, boundary_constraint);
     471             :         }
     472             : 
     473             :     } // end bid
     474        2556 : }
     475             : 
     476        4248 : void VariationalSmootherConstraint::fix_node(const Node & node)
     477             : {
     478       15708 :   for (const auto d : make_range(_sys.get_mesh().mesh_dimension()))
     479             :     {
     480       11460 :       const auto constrained_dof_index = node.dof_number(_sys.number(), d, 0);
     481        1910 :       DofConstraintRow constraint_row;
     482             :       // Leave the constraint row as all zeros so this dof is independent from other dofs
     483       11460 :       const auto constrained_value = node(d);
     484             :       // Simply constrain this dof to retain it's current value
     485       11460 :       _sys.get_dof_map().add_constraint_row(
     486             :           constrained_dof_index, constraint_row, constrained_value, true);
     487             :     }
     488        4248 : }
     489             : 
     490       12648 : void VariationalSmootherConstraint::constrain_node_to_plane(const Node & node,
     491             :                                                             const Point & ref_normal_vec)
     492             : {
     493       12648 :   const auto dim = _sys.get_mesh().mesh_dimension();
     494             :   // determine equation of plane: c_x * x + c_y * y + c_z * z + c = 0
     495        2108 :   std::vector<Real> xyz_coefs; // vector to hold c_x, c_y, c_z
     496        1054 :   Real c = 0.;
     497             : 
     498             :   // We choose to constrain the dimension with the largest magnitude coefficient
     499             :   // This approach ensures the coefficients added to the constraint_row
     500             :   // (i.e., -c_xyz / c_max) have as small magnitude as possible
     501             : 
     502             :   // We initialize this to avoid maybe-uninitialized compiler error
     503        1054 :   unsigned int constrained_dim = 0;
     504             : 
     505             :   // Let's assert that we have a nonzero normal to ensure that constrained_dim
     506             :   // is always set
     507        1054 :   libmesh_assert(ref_normal_vec.norm() > TOLERANCE);
     508             : 
     509        1054 :   Real max_abs_coef = 0.;
     510       50592 :   for (const auto d : make_range(dim))
     511             :     {
     512       37944 :       const auto coef = ref_normal_vec(d);
     513       37944 :       xyz_coefs.push_back(coef);
     514       37944 :       c -= coef * node(d);
     515             : 
     516        3162 :       const auto coef_abs = std::abs(coef);
     517       37944 :       if (coef_abs > max_abs_coef)
     518             :         {
     519        1054 :           max_abs_coef = coef_abs;
     520        1054 :           constrained_dim = d;
     521             :         }
     522             :     }
     523             : 
     524        2108 :   DofConstraintRow constraint_row;
     525       50592 :   for (const auto free_dim : make_range(dim))
     526             :     {
     527       37944 :       if (free_dim == constrained_dim)
     528       12648 :         continue;
     529             : 
     530       25296 :       const auto free_dof_index = node.dof_number(_sys.number(), free_dim, 0);
     531       29512 :       constraint_row[free_dof_index] = -xyz_coefs[free_dim] / xyz_coefs[constrained_dim];
     532             :     }
     533             : 
     534       12648 :   const auto inhomogeneous_part = -c / xyz_coefs[constrained_dim];
     535       12648 :   const auto constrained_dof_index = node.dof_number(_sys.number(), constrained_dim, 0);
     536       12648 :   _sys.get_dof_map().add_constraint_row(
     537             :       constrained_dof_index, constraint_row, inhomogeneous_part, true);
     538       12648 : }
     539             : 
     540        9264 : void VariationalSmootherConstraint::constrain_node_to_line(const Node & node,
     541             :                                                            const Point & line_vec)
     542             : {
     543        9264 :   const auto dim = _sys.get_mesh().mesh_dimension();
     544             : 
     545             :   // We will free the dimension most parallel to line_vec to keep the
     546             :   // constraint coefficients small
     547       10036 :   const std::vector<Real> line_vec_coefs{line_vec(0), line_vec(1), line_vec(2)};
     548        9264 :   auto it = std::max_element(line_vec_coefs.begin(), line_vec_coefs.end(), [](double a, double b) {
     549        1544 :     return std::abs(a) < std::abs(b);
     550        1544 :   });
     551        9264 :   const unsigned int free_dim = std::distance(line_vec_coefs.begin(), it);
     552        9264 :   const auto free_dof_index = node.dof_number(_sys.number(), free_dim, 0);
     553             : 
     554             :   // A line is parameterized as r(t) = node + t * line_vec, so
     555             :   // x(t) = node(x) + t * line_vec(x)
     556             :   // y(t) = node(y) + t * line_vec(y)
     557             :   // z(t) = node(z) + t * line_vec(z)
     558             :   // Let's say we leave x free. Then t = (x(t) - node(x)) / line_vec(x)
     559             :   // Then y and z can be constrained as
     560             :   // y = node(y) + line_vec_y * (x(t) - node(x)) / line_vec(x)
     561             :   //   = x(t) * line_vec(y) / line_vec(x) + (node(y) - node(x) * line_vec(y) / line_vec(x))
     562             :   // z = x(t) * line_vec(z) / line_vec(x) + (node(z) - node(x) * line_vec(z) / line_vec(x))
     563             : 
     564         772 :   libmesh_assert(!relative_fuzzy_equals(line_vec(free_dim), 0.));
     565       35160 :   for (const auto constrained_dim : make_range(dim))
     566             :     {
     567       25896 :       if (constrained_dim == free_dim)
     568        9264 :         continue;
     569             : 
     570        2772 :       DofConstraintRow constraint_row;
     571       16632 :       constraint_row[free_dof_index] = line_vec(constrained_dim) / line_vec(free_dim);
     572             :       const auto inhomogeneous_part =
     573       16632 :           node(constrained_dim) - node(free_dim) * line_vec(constrained_dim) / line_vec(free_dim);
     574       16632 :       const auto constrained_dof_index = node.dof_number(_sys.number(), constrained_dim, 0);
     575       16632 :       _sys.get_dof_map().add_constraint_row(
     576             :           constrained_dof_index, constraint_row, inhomogeneous_part, true);
     577             :     }
     578        9264 : }
     579             : 
     580             : // Utility function to determine whether two nodes share a boundary ID.
     581             : // The motivation for this is that a sliding boundary node on a triangular
     582             : // element can have a neighbor boundary node in the same element that is not
     583             : // part of the same boundary
     584             : // Consider the below example with nodes A, C, D, F, G that comprise elements
     585             : // E1, E2, E3, with boundaries B1, B2, B3, B4. To determine the constraint
     586             : // equations for the sliding node C, the neighboring nodes A and D need to
     587             : // be identified to construct the line that C is allowed to slide along.
     588             : // Note that neighbors A and D both share the boundary B1 with C.
     589             : // Without ensuring that neighbors share the same boundary as the current
     590             : // node, a neighboring node that does not lie on the same boundary
     591             : // (i.e. F and G) might be selected to define the constraining line,
     592             : // resulting in an incorrect constraint.
     593             : // Note that if, for example, boundaries B1 and B2 were to be combined
     594             : // into boundary B12, the node F would share a boundary id with node C
     595             : // and result in an incorrect constraint. It would be useful to design
     596             : // additional checks to detect cases like this.
     597             : 
     598             : //         B3
     599             : //    G-----------F
     600             : //    | \       / |
     601             : // B4 |  \  E2 /  | B2
     602             : //    |   \   /   |
     603             : //    | E1 \ / E3 |
     604             : //    A-----C-----D
     605             : //         B1
     606             : 
     607      177648 : bool VariationalSmootherConstraint::nodes_share_boundary_id(const Node & boundary_node,
     608             :                                                             const Node & neighbor_node,
     609             :                                                             const Elem & containing_elem,
     610             :                                                             const BoundaryInfo & boundary_info)
     611             : {
     612       14804 :   bool nodes_share_bid = false;
     613             : 
     614             :   // Node ids local to containing_elem
     615      162844 :   const auto node_id = containing_elem.get_node_index(&boundary_node);
     616      162844 :   const auto neighbor_id = containing_elem.get_node_index(&neighbor_node);
     617             : 
     618      540600 :   for (const auto side_id : containing_elem.side_index_range())
     619             :     {
     620             :       // We don't care about this side if it doesn't contain our boundary and neighbor nodes
     621      820092 :       if (!(containing_elem.is_node_on_side(node_id, side_id) &&
     622      305580 :             containing_elem.is_node_on_side(neighbor_id, side_id)))
     623      269088 :         continue;
     624             : 
     625             :       // If the current side, containing boundary_node and neighbor_node, lies on a boundary,
     626             :       // we can say that boundary_node and neighbor_node have a common boundary id.
     627       20452 :       std::vector<boundary_id_type> boundary_ids;
     628      245424 :       boundary_info.boundary_ids(&containing_elem, side_id, boundary_ids);
     629      245424 :       if (boundary_ids.size())
     630             :         {
     631       12630 :           nodes_share_bid = true;
     632       12630 :           break;
     633             :         }
     634             :     }
     635      177648 :   return nodes_share_bid;
     636             : }
     637             : 
     638             : std::set<std::set<const Node *>>
     639        2424 : VariationalSmootherConstraint::get_neighbors_for_subdomain_constraint(
     640             :     const MeshBase & mesh,
     641             :     const Node & node,
     642             :     const subdomain_id_type sub_id,
     643             :     const std::unordered_map<dof_id_type, std::vector<const Elem *>> & nodes_to_elem_map)
     644             : {
     645             : 
     646             :   // Find all the nodal neighbors... that is the nodes directly connected
     647             :   // to this node through one edge, or if none exists, use other nodes on the
     648             :   // containing face
     649         404 :   std::vector<const Node *> neighbors;
     650        2424 :   MeshTools::find_nodal_or_face_neighbors(
     651             :       mesh, node, nodes_to_elem_map, neighbors);
     652             : 
     653             :   // Each constituent set corresponds to neighbors sharing a face on the
     654             :   // subdomain boundary
     655         202 :   std::set<std::set<const Node *>> side_grouped_boundary_neighbors;
     656             : 
     657       12096 :   for (const auto * neigh : neighbors)
     658             :     {
     659             :       // Determine whether the neighbor is on the subdomain boundary
     660             :       // First, find the common elements that both node and neigh belong to
     661        9672 :       const auto & elems_containing_node = libmesh_map_find(nodes_to_elem_map, node.id());
     662        9672 :       const auto & elems_containing_neigh = libmesh_map_find(nodes_to_elem_map, neigh->id());
     663         806 :       const Elem * common_elem = nullptr;
     664       47415 :       for (const auto * neigh_elem : elems_containing_neigh)
     665             :         {
     666       43957 :           if ((std::find(elems_containing_node.begin(), elems_containing_node.end(), neigh_elem) !=
     667        6364 :                elems_containing_node.end())
     668             :               // We should be able to find a common element on the sub_id boundary
     669       37743 :               && (neigh_elem->subdomain_id() == sub_id))
     670         838 :             common_elem = neigh_elem;
     671             :           else
     672       27509 :             continue;
     673             : 
     674             :           // Now, determine whether node and neigh are on a side coincident
     675             :           // with the subdomain boundary
     676       56124 :           for (const auto common_side : common_elem->side_index_range())
     677             :             {
     678        3746 :               bool node_found_on_side = false;
     679        3746 :               bool neigh_found_on_side = false;
     680      367136 :               for (const auto local_node_id : common_elem->nodes_on_side(common_side))
     681             :                 {
     682      343326 :                   if (common_elem->node_id(local_node_id) == node.id())
     683        1692 :                     node_found_on_side = true;
     684      296696 :                   else if (common_elem->node_id(local_node_id) == neigh->id())
     685        1974 :                     neigh_found_on_side = true;
     686             :                 }
     687             : 
     688       47140 :               if (!(node_found_on_side && neigh_found_on_side &&
     689        2500 :                     common_elem->neighbor_ptr(common_side)))
     690       31328 :                 continue;
     691             : 
     692         954 :               const auto matched_side = common_side;
     693             :               // There could be multiple matched sides, so keep this next part
     694             :               // inside the common_side loop
     695             :               //
     696             :               // Does matched_side, containing both node and neigh, lie on the
     697             :               // sub_id subdomain boundary?
     698             :               const auto matched_neighbor_sub_id =
     699        1908 :                   common_elem->neighbor_ptr(matched_side)->subdomain_id();
     700         954 :               const bool is_matched_side_on_subdomain_boundary = matched_neighbor_sub_id != sub_id;
     701             : 
     702       11770 :               if (is_matched_side_on_subdomain_boundary)
     703             :                 {
     704             :                   // Store all nodes that live on this side
     705        8670 :                   const auto nodes_on_side = common_elem->nodes_on_side(common_side);
     706        1296 :                   std::set<const Node *> node_ptrs_on_side;
     707       65080 :                   for (const auto local_node_id : nodes_on_side)
     708       61650 :                     node_ptrs_on_side.insert(common_elem->node_ptr(local_node_id));
     709        8670 :                   node_ptrs_on_side.erase(node_ptrs_on_side.find(&node));
     710        7374 :                   side_grouped_boundary_neighbors.insert(node_ptrs_on_side);
     711             : 
     712         648 :                   continue;
     713             :                 }
     714             : 
     715             :             } // for common_side
     716             : 
     717             :         } // for neigh_elem
     718             :     }
     719             : 
     720        2626 :   return side_grouped_boundary_neighbors;
     721             : }
     722             : 
     723             : std::set<std::set<const Node *>>
     724       25248 : VariationalSmootherConstraint::get_neighbors_for_boundary_constraint(
     725             :     const MeshBase & mesh,
     726             :     const Node & node,
     727             :     const std::unordered_set<dof_id_type> & boundary_node_ids,
     728             :     const BoundaryInfo & boundary_info,
     729             :     const std::unordered_map<dof_id_type, std::vector<const Elem *>> & nodes_to_elem_map)
     730             : {
     731             : 
     732             :   // Find all the nodal neighbors... that is the nodes directly connected
     733             :   // to this node through one edge, or if none exists, use other nodes on the
     734             :   // containing face
     735        4208 :   std::vector<const Node *> neighbors;
     736       25248 :   MeshTools::find_nodal_or_face_neighbors(
     737             :       mesh, node, nodes_to_elem_map, neighbors);
     738             : 
     739             :   // Each constituent set corresponds to neighbors sharing a face on the
     740             :   // boundary
     741        2104 :   std::set<std::set<const Node *>> side_grouped_boundary_neighbors;
     742             : 
     743      141192 :   for (const auto * neigh : neighbors)
     744             :     {
     745             :       const bool is_neighbor_boundary_node =
     746      115944 :           boundary_node_ids.find(neigh->id()) != boundary_node_ids.end();
     747      115944 :       if (!is_neighbor_boundary_node)
     748       20152 :         continue;
     749             : 
     750             :       // Determine whether nodes share a common boundary id
     751             :       // First, find the common element that both node and neigh belong to
     752       93960 :       const auto & elems_containing_node = libmesh_map_find(nodes_to_elem_map, node.id());
     753       93960 :       const auto & elems_containing_neigh = libmesh_map_find(nodes_to_elem_map, neigh->id());
     754        7830 :       const Elem * common_elem = nullptr;
     755      565736 :       for (const auto * neigh_elem : elems_containing_neigh)
     756             :         {
     757             :           const bool is_neigh_common =
     758      511994 :               std::find(elems_containing_node.begin(), elems_containing_node.end(), neigh_elem) !=
     759       80436 :               elems_containing_node.end();
     760      471776 :           if (!is_neigh_common)
     761      268714 :             continue;
     762       29608 :           common_elem = neigh_elem;
     763             :           // Keep this in the neigh_elem loop because there can be multiple common
     764             :           // elements Now, determine whether node and neigh share a common boundary
     765             :           // id
     766      177648 :           const bool nodes_have_common_bid = VariationalSmootherConstraint::nodes_share_boundary_id(
     767             :               node, *neigh, *common_elem, boundary_info);
     768      177648 :           if (nodes_have_common_bid)
     769             :             {
     770             :               // Find the side coinciding with the shared boundary
     771      885528 :               for (const auto side : common_elem->side_index_range())
     772             :                 {
     773             :                   // We only care about external boundaries here, make sure side doesn't
     774             :                   // have a neighbor
     775      795132 :                   if (common_elem->neighbor_ptr(side))
     776      570912 :                     continue;
     777             : 
     778       20412 :                   bool node_found_on_side = false;
     779       20412 :                   bool neigh_found_on_side = false;
     780      244944 :                   const auto nodes_on_side = common_elem->nodes_on_side(side);
     781     1754256 :                   for (const auto local_node_id : nodes_on_side)
     782             :                     {
     783     1635088 :                       if (common_elem->node_id(local_node_id) == node.id())
     784       14712 :                         node_found_on_side = true;
     785     1332768 :                       else if (common_elem->node_id(local_node_id) == neigh->id())
     786       15396 :                         neigh_found_on_side = true;
     787             :                     }
     788      244944 :                   if (!(node_found_on_side && neigh_found_on_side))
     789        6824 :                     continue;
     790             : 
     791       27176 :                   std::set<const Node *> node_ptrs_on_side;
     792     1089744 :                   for (const auto local_node_id : nodes_on_side)
     793     1003912 :                     node_ptrs_on_side.insert(common_elem->node_ptr(local_node_id));
     794      176644 :                   node_ptrs_on_side.erase(node_ptrs_on_side.find(&node));
     795      149468 :                   side_grouped_boundary_neighbors.insert(node_ptrs_on_side);
     796             :                 }
     797      138930 :               continue;
     798      126300 :             }
     799             :         }
     800             :     }
     801             : 
     802       27352 :   return side_grouped_boundary_neighbors;
     803             : }
     804             : 
     805       27672 : ConstraintVariant VariationalSmootherConstraint::determine_constraint(
     806             :     const Node & node,
     807             :     const unsigned int dim,
     808             :     const std::set<std::set<const Node *>> & side_grouped_boundary_neighbors)
     809             : {
     810             :   // Determines the appropriate geometric constraint for a node based on its
     811             :   // neighbors.
     812             : 
     813             :   // Extract neighbors in flat vector
     814        4612 :   std::vector<const Node *> neighbors;
     815      108902 :   for (const auto & side : side_grouped_boundary_neighbors)
     816       81230 :     neighbors.insert(neighbors.end(), side.begin(), side.end());
     817             : 
     818             :   // Constrain the node to it's current location
     819             :   // Note that neighbors.size() <= 1 is used here instead of == 1 so we don't
     820             :   // crash when trying to access *neighbors[0] a few lines below in the
     821             :   // dim == 2 case
     822       27672 :   if (dim == 1 || neighbors.size() <= 1)
     823         490 :     return PointConstraint{node};
     824             : 
     825       27252 :   if (dim == 2 || neighbors.size() == 2)
     826             :     {
     827             :       // Determine whether node + all neighbors are colinear
     828         263 :       bool all_colinear = true;
     829        3419 :       const Point ref_dir = (*neighbors[0] - node).unit();
     830        6171 :       for (const auto i : make_range(std::size_t(1), neighbors.size()))
     831             :         {
     832        4059 :           const Point delta = *(neighbors[i]) - node;
     833         312 :           libmesh_assert(delta.norm() >= TOLERANCE);
     834        3747 :           const Point dir = delta.unit();
     835        4022 :           if (!dir.relative_fuzzy_equals(ref_dir) && !dir.relative_fuzzy_equals(-ref_dir))
     836             :           {
     837          61 :             all_colinear = false;
     838         732 :             break;
     839             :           }
     840             :         }
     841             : 
     842         263 :       if (all_colinear)
     843        2828 :         return LineConstraint{node, ref_dir};
     844             : 
     845         854 :       return PointConstraint{node};
     846             :     }
     847             : 
     848             :   // dim == 3, neighbors.size() >= 3
     849        4016 :   std::set<PlaneConstraint> valid_planes;
     850       99040 :   for (const auto & side_nodes : side_grouped_boundary_neighbors)
     851             :     {
     852       81180 :       std::vector<const Node *> side_nodes_vec(side_nodes.begin(), side_nodes.end());
     853      422916 :       for (const auto i : index_range(side_nodes_vec))
     854             :         {
     855      376900 :           const Point vec_i = (*side_nodes_vec[i] - node);
     856     1183902 :           for (const auto j : make_range(i))
     857             :             {
     858      905362 :               const Point vec_j = (*side_nodes_vec[j] - node);
     859      766498 :               Point candidate_normal = vec_i.cross(vec_j);
     860      835930 :               if (candidate_normal.norm() <= TOLERANCE)
     861       54352 :                 continue;
     862             : 
     863      781578 :               const PlaneConstraint candidate_plane{node, candidate_normal.unit()};
     864      716661 :               valid_planes.emplace(candidate_plane);
     865             :             }
     866             :         }
     867             :     }
     868             : 
     869             :   // Fall back to point constraint
     870       24096 :   if (valid_planes.empty())
     871           0 :     return PointConstraint(node);
     872             : 
     873             :   // Combine all the planes together to get a common constraint
     874        2008 :   auto it = valid_planes.begin();
     875        4016 :   ConstraintVariant current = *it++;
     876       51368 :   for (; it != valid_planes.end(); ++it)
     877             :     {
     878       29530 :       current = intersect_constraints(current, *it);
     879             : 
     880             :       // This will catch cases where constraints have no intersection
     881             :       // (i.e., the element surface is non-planar)
     882             :       // Fall back to fixed node constraint
     883       27272 :       if (std::holds_alternative<InvalidConstraint>(current))
     884             :         {
     885           0 :           current = PointConstraint(node);
     886           0 :           break;
     887             :         }
     888             :     }
     889             : 
     890        2008 :   return current;
     891             : }
     892             : 
     893             : // Applies the computed constraint (PointConstraint, LineConstraint, or
     894             : // PlaneConstraint) to a node.
     895       26160 : void VariationalSmootherConstraint::impose_constraint(const Node & node,
     896             :                                                       const ConstraintVariant & constraint)
     897             : {
     898             : 
     899        2180 :   libmesh_assert_msg(!std::holds_alternative<InvalidConstraint>(constraint),
     900             :                      "Cannot impose constraint using InvalidConstraint.");
     901             : 
     902       26160 :   if (std::holds_alternative<PointConstraint>(constraint))
     903        4248 :     fix_node(node);
     904       21912 :   else if (std::holds_alternative<LineConstraint>(constraint))
     905        9264 :     constrain_node_to_line(node, std::get<LineConstraint>(constraint).direction());
     906       12648 :   else if (std::holds_alternative<PlaneConstraint>(constraint))
     907       12648 :     constrain_node_to_plane(node, std::get<PlaneConstraint>(constraint).normal());
     908             : 
     909             :   else
     910           0 :     libmesh_assert_msg(false, "Unknown constraint type.");
     911       26160 : }
     912             : 
     913             : } // namespace libMesh

Generated by: LCOV version 1.14