LCOV - code coverage report
Current view: top level - src/userobjects - PenaltyWeightedGapUserObject.C (source / functions) Hit Total Coverage
Test: idaholab/moose contact: #33390 (250e9c) with base 846a5c Lines: 178 191 93.2 %
Date: 2026-07-31 18:16:12 Functions: 21 21 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : //* This file is part of the MOOSE framework
       2             : //* https://mooseframework.inl.gov
       3             : //*
       4             : //* All rights reserved, see COPYRIGHT for full restrictions
       5             : //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
       6             : //*
       7             : //* Licensed under LGPL 2.1, please see LICENSE for details
       8             : //* https://www.gnu.org/licenses/lgpl-2.1.html
       9             : 
      10             : #include "PenaltyWeightedGapUserObject.h"
      11             : #include "AugmentedLagrangianContactProblem.h"
      12             : #include "MooseVariableFE.h"
      13             : #include "SystemBase.h"
      14             : 
      15             : registerMooseObject("ContactApp", PenaltyWeightedGapUserObject);
      16             : 
      17             : const unsigned int PenaltyWeightedGapUserObject::_no_iterations = 0;
      18             : 
      19             : InputParameters
      20         458 : PenaltyWeightedGapUserObject::validParams()
      21             : {
      22         458 :   InputParameters params = WeightedGapUserObject::validParams();
      23         458 :   params.set<bool>("allow_nodal_normal_derivatives") = true;
      24         458 :   params.set<bool>("use_nodal_normal_derivatives") = true;
      25         916 :   params.suppressParameter<bool>("ghost_point_neighbors");
      26             :   // Penalty contact has no LM constraint to contribute the nodal-normal one-ring to the matrix
      27             :   // graph, so its weighted-gap object supplies the coupling relationship manager directly.
      28             :   const auto configure_one_ring =
      29        1062 :       [](const InputParameters & obj_params, InputParameters & rm_params)
      30             :   {
      31        1062 :     rm_params.set<bool>("use_displaced_mesh") = obj_params.get<bool>("use_displaced_mesh");
      32        1062 :     rm_params.set<BoundaryName>("secondary_boundary") =
      33        2124 :         obj_params.get<BoundaryName>("secondary_boundary");
      34        1062 :     rm_params.set<BoundaryName>("primary_boundary") =
      35        2124 :         obj_params.get<BoundaryName>("primary_boundary");
      36        1062 :     rm_params.set<SubdomainName>("secondary_subdomain") =
      37        2124 :         obj_params.get<SubdomainName>("secondary_subdomain");
      38        1062 :     rm_params.set<SubdomainName>("primary_subdomain") =
      39        2124 :         obj_params.get<SubdomainName>("primary_subdomain");
      40             :     // penetration_tolerance is required exactly for augmented-Lagrange contact, which uses
      41             :     // frozen-normal geometry.
      42        1062 :     rm_params.set<bool>("ghost_point_neighbors") =
      43        1854 :         obj_params.get<bool>("use_nodal_normal_derivatives") &&
      44        1854 :         !obj_params.isParamValid("penetration_tolerance");
      45        1062 :   };
      46         916 :   params.addRelationshipManager("AugmentSparsityOnInterface",
      47             :                                 Moose::RelationshipManagerType::GEOMETRIC |
      48             :                                     Moose::RelationshipManagerType::ALGEBRAIC,
      49             :                                 configure_one_ring);
      50         916 :   params.addRelationshipManager(
      51             :       "AugmentSparsityOnInterface", Moose::RelationshipManagerType::COUPLING, configure_one_ring);
      52         458 :   params.addClassDescription("Computes the mortar normal contact force via a penalty approach.");
      53         916 :   params.addRequiredParam<Real>("penalty", "The penalty factor");
      54        1374 :   params.addRangeCheckedParam<Real>(
      55             :       "penalty_multiplier",
      56         916 :       1.0,
      57             :       "penalty_multiplier > 0",
      58             :       "The penalty growth factor between augmented Lagrange "
      59             :       "iterations if weighted gap does not get closed fast enough. For frictional simulations, "
      60             :       "values smaller than 100 are recommended, e.g. 5.");
      61         916 :   params.addParam<bool>(
      62             :       "use_physical_gap",
      63         916 :       false,
      64             :       "Whether to use the physical normal gap (not scaled by mortar integration) and normalize the "
      65             :       "penalty coefficient with a representative lower-dimensional volume assigned to the node in "
      66             :       "the contacting boundary. This parameter is defaulted to 'true' in the contact action.");
      67         916 :   params.addRangeCheckedParam<Real>(
      68             :       "penetration_tolerance",
      69             :       "penetration_tolerance > 0",
      70             :       "Acceptable penetration distance at which augmented Lagrange iterations can be stopped");
      71        1374 :   params.addRangeCheckedParam<Real>(
      72             :       "max_penalty_multiplier",
      73         916 :       1.0e3,
      74             :       "max_penalty_multiplier >= 1.0",
      75             :       "Maximum multiplier applied to penalty factors when adaptivity is used in an augmented "
      76             :       "Lagrange setting. The penalty factor supplied by the user is used as a reference.");
      77         916 :   MooseEnum adaptivity_penalty_normal("SIMPLE BUSSETTA", "SIMPLE");
      78         916 :   adaptivity_penalty_normal.addDocumentation(
      79             :       "SIMPLE", "Keep multiplying by the penalty multiplier between AL iterations");
      80         916 :   adaptivity_penalty_normal.addDocumentation(
      81             :       "BUSSETTA",
      82             :       "Modify the penalty using an algorithm from Bussetta et al, 2012, Comput Mech 49:259-275 "
      83             :       "between AL iterations.");
      84         916 :   params.addParam<MooseEnum>(
      85             :       "adaptivity_penalty_normal",
      86             :       adaptivity_penalty_normal,
      87             :       "The augmented Lagrange update strategy used on the normal penalty coefficient.");
      88         916 :   params.addCoupledVar(
      89             :       "aux_lm",
      90             :       "Auxiliary variable that is utilized together with the "
      91             :       "penalty approach to interpolate the resulting contact tractions using dual bases.");
      92         916 :   params.addParamNamesToGroup("penalty_multiplier penetration_tolerance max_penalty_multiplier",
      93             :                               "Augmented Lagrange");
      94             : 
      95         458 :   return params;
      96         458 : }
      97             : 
      98         230 : PenaltyWeightedGapUserObject::PenaltyWeightedGapUserObject(const InputParameters & parameters)
      99             :   : WeightedGapUserObject(parameters),
     100             :     AugmentedLagrangeInterface(this),
     101         228 :     _penalty(getParam<Real>("penalty")),
     102         456 :     _penalty_multiplier(getParam<Real>("penalty_multiplier")),
     103         228 :     _penetration_tolerance(
     104         482 :         isParamValid("penetration_tolerance") ? getParam<Real>("penetration_tolerance") : 0.0),
     105         228 :     _augmented_lagrange_problem(
     106         228 :         dynamic_cast<AugmentedLagrangianContactProblemInterface *>(&_fe_problem)),
     107         228 :     _lagrangian_iteration_number(_augmented_lagrange_problem
     108         228 :                                      ? _augmented_lagrange_problem->getLagrangianIterationNumber()
     109             :                                      : _no_iterations),
     110         228 :     _dt(_fe_problem.dt()),
     111         456 :     _use_physical_gap(getParam<bool>("use_physical_gap")),
     112         264 :     _aux_lm_var(isCoupled("aux_lm") ? getVar("aux_lm", 0) : nullptr),
     113         456 :     _max_penalty_multiplier(getParam<Real>("max_penalty_multiplier")),
     114         228 :     _adaptivity_normal(
     115         458 :         getParam<MooseEnum>("adaptivity_penalty_normal").getEnum<AdaptivityNormalPenalty>())
     116             : {
     117         485 :   auto check_type = [this](const auto & var, const auto & var_name)
     118             :   {
     119         485 :     if (!var.isNodal())
     120           0 :       paramError(var_name,
     121             :                  "The displacement variables must have degrees of freedom exclusively on "
     122             :                  "nodes, e.g. they should probably be of finite element type 'Lagrange'.");
     123         713 :   };
     124         228 :   check_type(*_disp_x_var, "disp_x");
     125         228 :   check_type(*_disp_y_var, "disp_y");
     126         228 :   if (_has_disp_z)
     127          29 :     check_type(*_disp_z_var, "disp_z");
     128             : 
     129         456 :   if (!_augmented_lagrange_problem == isParamValid("penetration_tolerance"))
     130           0 :     paramError("penetration_tolerance",
     131             :                "This parameter must be supplied if and only if an augmented Lagrange problem "
     132             :                "object is used.");
     133         228 : }
     134             : 
     135             : const VariableTestValue &
     136         106 : PenaltyWeightedGapUserObject::test() const
     137             : {
     138         106 :   return _aux_lm_var ? _aux_lm_var->phiLower() : _disp_x_var->phiLower();
     139             : }
     140             : 
     141             : const ADVariableValue &
     142     8238288 : PenaltyWeightedGapUserObject::contactPressure() const
     143             : {
     144     8238288 :   return _contact_pressure;
     145             : }
     146             : 
     147             : void
     148       22610 : PenaltyWeightedGapUserObject::selfInitialize()
     149             : {
     150      116718 :   for (auto & dof_pn : _dof_to_normal_pressure)
     151       94108 :     dof_pn.second = 0.0;
     152       22610 : }
     153             : 
     154             : void
     155        7652 : PenaltyWeightedGapUserObject::initialize()
     156             : {
     157        7652 :   WeightedGapUserObject::initialize();
     158        7652 :   selfInitialize();
     159        7652 : }
     160             : 
     161             : void
     162       22610 : PenaltyWeightedGapUserObject::selfFinalize()
     163             : {
     164             :   using std::min;
     165             : 
     166             :   // compute new normal pressure for each node
     167      117794 :   for (const auto & [dof_object, wgap] : _dof_to_weighted_gap)
     168             :   {
     169       95184 :     auto penalty = findValue(_dof_to_local_penalty, dof_object, _penalty);
     170             : 
     171             :     // If _use_physical_gap is true we "normalize" the penalty parameter with the surrounding area.
     172       95184 :     auto gap = _use_physical_gap ? adPhysicalGap(wgap) / wgap.second : wgap.first;
     173             : 
     174             :     const auto lagrange_multiplier =
     175      144412 :         _augmented_lagrange_problem ? _dof_to_lagrange_multiplier[dof_object] : 0.0;
     176             : 
     177             :     // keep the negative normal pressure (compressive per convention here)
     178       95184 :     auto normal_pressure = min(0.0, penalty * gap + lagrange_multiplier);
     179             : 
     180             :     // we switch conventins here and consider positive normal pressure as compressive
     181      190368 :     _dof_to_normal_pressure[dof_object] = -normal_pressure;
     182             :   }
     183       22610 : }
     184             : 
     185             : void
     186        7652 : PenaltyWeightedGapUserObject::finalize()
     187             : {
     188        7652 :   WeightedGapUserObject::finalize();
     189        7652 :   selfFinalize();
     190        7652 : }
     191             : 
     192             : Real
     193      135418 : PenaltyWeightedGapUserObject::getNormalContactPressure(const Node * const node) const
     194             : {
     195      135418 :   const auto it = _dof_to_normal_pressure.find(_subproblem.mesh().nodePtr(node->id()));
     196             : 
     197      135418 :   if (it != _dof_to_normal_pressure.end())
     198        5830 :     return MetaPhysicL::raw_value(it->second);
     199             :   else
     200             :     return 0.0;
     201             : }
     202             : 
     203             : Real
     204         810 : PenaltyWeightedGapUserObject::getNormalLagrangeMultiplier(const Node * const node) const
     205             : {
     206         810 :   const auto it = _dof_to_lagrange_multiplier.find(_subproblem.mesh().nodePtr(node->id()));
     207             : 
     208         810 :   if (it != _dof_to_lagrange_multiplier.end())
     209         710 :     return -MetaPhysicL::raw_value(it->second);
     210             :   else
     211             :     return 0.0;
     212             : }
     213             : 
     214             : void
     215      171513 : PenaltyWeightedGapUserObject::reinit()
     216             : {
     217      171513 :   _contact_pressure.resize(_qrule_msm->n_points());
     218      651891 :   for (const auto qp : make_range(_qrule_msm->n_points()))
     219      480378 :     _contact_pressure[qp] = 0.0;
     220             : 
     221      651891 :   for (const auto i : make_range(_test->size()))
     222             :   {
     223      480378 :     const Node * const node = _lower_secondary_elem->node_ptr(i);
     224             : 
     225     1990542 :     for (const auto qp : make_range(_qrule_msm->n_points()))
     226     3020328 :       _contact_pressure[qp] += (*_test)[i][qp] * _dof_to_normal_pressure[node];
     227             :   }
     228      171513 : }
     229             : 
     230             : void
     231        1494 : PenaltyWeightedGapUserObject::selfTimestepSetup()
     232             : {
     233             :   // Let's not clear the LMs for improved performance in
     234             :   // nonlinear problems
     235             : 
     236             :   // reset penalty
     237        1565 :   for (auto & dof_lp : _dof_to_local_penalty)
     238          71 :     dof_lp.second = _penalty;
     239             : 
     240             :   // clear previous gap
     241        1565 :   for (auto & dof_pg : _dof_to_previous_gap)
     242          71 :     dof_pg.second = 0.0;
     243             : 
     244             :   // save old timestep
     245        1494 :   _dt_old = _dt;
     246        1494 : }
     247             : 
     248             : void
     249         689 : PenaltyWeightedGapUserObject::timestepSetup()
     250             : {
     251         689 :   selfTimestepSetup();
     252         689 : }
     253             : 
     254             : bool
     255          74 : PenaltyWeightedGapUserObject::isAugmentedLagrangianConverged()
     256             : {
     257             :   using std::max, std::min, std::abs;
     258             : 
     259          74 :   Real max_positive_gap = 0.0;
     260          74 :   Real min_negative_gap = 0.0;
     261             : 
     262             :   // Get maximum gap to ascertain whether we are converged.
     263        3788 :   for (auto & [dof_object, wgap] : _dof_to_weighted_gap)
     264             :   {
     265        3714 :     const auto gap = physicalGap(wgap);
     266             :     {
     267             :       // Check condition for nodes that are active
     268        7232 :       if (gap < 0 || _dof_to_lagrange_multiplier[dof_object] < 0.0)
     269             :       {
     270         282 :         max_positive_gap = max(max_positive_gap, gap);
     271         282 :         min_negative_gap = min(min_negative_gap, gap);
     272             :       }
     273             :     }
     274             :   }
     275             : 
     276             :   // Communicate the extreme gap values in parallel.
     277             :   std::vector<Real> recv;
     278          74 :   if (this->_communicator.rank() == 0)
     279          52 :     recv.resize(this->_communicator.size());
     280          74 :   this->_communicator.gather(
     281          74 :       0, -min_negative_gap > max_positive_gap ? min_negative_gap : max_positive_gap, recv);
     282             : 
     283          74 :   if (this->_communicator.rank() == 0)
     284             :   {
     285          52 :     min_negative_gap = *std::min_element(recv.begin(), recv.end());
     286          52 :     max_positive_gap = *std::max_element(recv.begin(), recv.end());
     287             : 
     288             :     // report the gap value with the largest magnitude
     289          52 :     const Real reported_gap =
     290          52 :         -min_negative_gap > max_positive_gap ? min_negative_gap : max_positive_gap;
     291          52 :     if (abs(reported_gap) > _penetration_tolerance)
     292             :     {
     293           9 :       mooseInfoRepeated("Penetration tolerance fail max_gap = ",
     294             :                         reported_gap,
     295             :                         " (gap_tol=",
     296           9 :                         _penetration_tolerance,
     297             :                         "). Iteration number is: ",
     298             :                         _lagrangian_iteration_number,
     299             :                         ".");
     300           9 :       return false;
     301             :     }
     302             :     else
     303          43 :       mooseInfoRepeated("Penetration tolerance success max_gap = ",
     304             :                         reported_gap,
     305             :                         " (gap_tol=",
     306          43 :                         _penetration_tolerance,
     307             :                         "). Iteration number is: ",
     308             :                         _lagrangian_iteration_number,
     309             :                         ".");
     310             :   }
     311             : 
     312             :   return true;
     313          74 : }
     314             : 
     315             : void
     316          60 : PenaltyWeightedGapUserObject::augmentedLagrangianSetup()
     317             : {
     318             :   // Loop over all nodes for which a gap has been computed
     319        3060 :   for (auto & [dof_object, wgap] : _dof_to_weighted_gap)
     320             :   {
     321             :     const Real gap = physicalGap(wgap);
     322             :     // Store previous augmented lagrange iteration gap
     323        3000 :     _dof_to_previous_gap[dof_object] = gap;
     324             :   }
     325          60 : }
     326             : 
     327             : void
     328          60 : PenaltyWeightedGapUserObject::updateAugmentedLagrangianMultipliers()
     329             : {
     330             :   using std::abs;
     331             : 
     332        3060 :   for (const auto & [dof_object, wgap] : _dof_to_weighted_gap)
     333             :   {
     334             :     auto & penalty = _dof_to_local_penalty[dof_object];
     335        3000 :     if (penalty == 0.0)
     336         643 :       penalty = _penalty;
     337             : 
     338        3000 :     const auto gap = getNormalGap(static_cast<const Node *>(dof_object));
     339             :     auto & lagrange_multiplier = _dof_to_lagrange_multiplier[dof_object];
     340             : 
     341             :     const auto possible_normalization =
     342        3000 :         (_use_physical_gap ? libmesh_map_find(_dof_to_weighted_gap, dof_object).second : 1.0);
     343             : 
     344             :     // Update penalty (the factor of 1/4 is suggested in the literature, the limit on AL iteration
     345             :     // caps the penalty increase)
     346             :     // Before we were updating the LM before adapting the penalty factor
     347        3000 :     if (lagrange_multiplier + gap * penalty / possible_normalization <= 0)
     348         228 :       lagrange_multiplier += gap * penalty / possible_normalization;
     349             :     else
     350        2772 :       lagrange_multiplier = 0.0;
     351             : 
     352        3000 :     const auto previous_gap = _dof_to_previous_gap[dof_object];
     353        3000 :     Real eval_tn = 0;
     354             : 
     355        3000 :     if (_adaptivity_normal == AdaptivityNormalPenalty::SIMPLE)
     356             :     {
     357           0 :       if (abs(gap) > 0.25 * abs(previous_gap) && abs(gap) > _penetration_tolerance)
     358           0 :         penalty *= _penalty_multiplier;
     359             :     }
     360        3000 :     else if (_adaptivity_normal == AdaptivityNormalPenalty::BUSSETTA)
     361        3000 :       bussettaAdaptivePenalty(previous_gap, gap, penalty, eval_tn);
     362             : 
     363             :     // Heuristics to bound the penalty factor
     364        3000 :     if (penalty < _penalty)
     365           0 :       penalty = _penalty;
     366        3000 :     else if (penalty > _max_penalty_multiplier * _penalty)
     367           0 :       penalty = _max_penalty_multiplier * _penalty;
     368             :   }
     369          60 : }
     370             : 
     371             : void
     372        3000 : PenaltyWeightedGapUserObject::bussettaAdaptivePenalty(const Real previous_gap,
     373             :                                                       const Real gap,
     374             :                                                       Real & penalty,
     375             :                                                       Real & eval_tn)
     376             : {
     377             :   // Positive gaps means no contact
     378        3000 :   if (previous_gap > 0.0)
     379             :   {
     380        2226 :     penalty = _penalty;
     381        2226 :     eval_tn = 0.0;
     382             :   }
     383             :   else
     384             :   {
     385         774 :     if (previous_gap * gap < 0)
     386          18 :       eval_tn = 0.0;
     387             :     else
     388         756 :       eval_tn = penalty * previous_gap;
     389             : 
     390         774 :     adaptiveNormalPenalty(previous_gap, gap, penalty);
     391             :   }
     392        3000 : }
     393             : 
     394             : void
     395         774 : PenaltyWeightedGapUserObject::adaptiveNormalPenalty(const Real previous_gap,
     396             :                                                     const Real gap,
     397             :                                                     Real & penalty)
     398             : {
     399             :   using std::abs, std::max, std::sqrt;
     400             : 
     401         774 :   const bool condition_one = abs(abs(previous_gap / gap) - 1.0) < 0.01;
     402         774 :   const bool condition_two = abs(gap) > 1.01 * abs(previous_gap);
     403             : 
     404         774 :   if (previous_gap * gap < 0)
     405             :   {
     406          18 :     if (previous_gap > _penetration_tolerance)
     407           0 :       penalty = abs(penalty * previous_gap / gap * (abs(gap) + _penetration_tolerance) /
     408           0 :                     (gap - previous_gap));
     409             :     else
     410          18 :       penalty = abs(penalty * previous_gap / 10.0 / gap);
     411             :   }
     412         756 :   else if (abs(gap) > _penetration_tolerance)
     413             :   {
     414         643 :     if (abs(gap - previous_gap) >
     415        1335 :         max(gap / 10.0, max(previous_gap / 10.0, 5.0 * _penetration_tolerance)))
     416         643 :       penalty *= 10.0;
     417           0 :     else if (condition_one && gap < 10.0 * _penetration_tolerance)
     418           0 :       penalty *= MathUtils::pow(sqrt(abs(gap) / _penetration_tolerance - 1.0) + 1.0, 2);
     419           0 :     else if (condition_two)
     420           0 :       penalty *= 10.0 * previous_gap / gap;
     421             :     else
     422           0 :       penalty *= sqrt(abs(gap) / _penetration_tolerance - 1.0) + 1.0;
     423             :   }
     424         774 : }

Generated by: LCOV version 1.14