diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index dd770ad3..359b870d 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -33,6 +33,7 @@ set( tribol_tests tribol_mortar_wts.cpp tribol_nodal_nrmls.cpp tribol_quad_integ.cpp + tribol_residual_gap.cpp tribol_tet_mesh.cpp tribol_timestep_vote.cpp tribol_twb_integ.cpp diff --git a/src/tests/tribol_enzyme_element_mortar.cpp b/src/tests/tribol_enzyme_element_mortar.cpp index 34b05912..7b000706 100644 --- a/src/tests/tribol_enzyme_element_mortar.cpp +++ b/src/tests/tribol_enzyme_element_mortar.cpp @@ -969,6 +969,43 @@ TEST_F( EnzymeElementMortarTest, NoOverlap ) FDCheck( x1, x2, n1, p1, x1_stencil, x2_stencil ); } +TEST_F( EnzymeElementMortarTest, ExactOverlapResidualGap ) +{ + // clang-format off + // Setup two overlapping elements with a gap of 0.1 + double x1[12] = { 0.0, 1.0, 1.0, 0.0, + 0.0, 0.0, 1.0, 1.0, + 0.0, 0.0, 0.0, 0.0 }; + // Define x2 in CW order so ElemReverse makes it CCW + double x2[12] = { 0.0, 0.0, 1.0, 1.0, + 0.0, 1.0, 1.0, 0.0, + 0.1, 0.1, 0.1, 0.1 }; + double n1[12] = { 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, + 1.0, 1.0, 1.0, 1.0 }; + double p1[4] = { 1.0, 1.0, 1.0, 1.0 }; + // clang-format on + + double residual_gap = 0.15; + + double f1[12], f2[12], g1[4]; + int num_nodes = 4; + + // Call with residual_gap + tribol::ComputeMortarForceEnzyme( x1, n1, p1, f1, g1, num_nodes, x2, f2, num_nodes, residual_gap ); + + // Kinematic gap is 0.1. Effective gap is 0.1 - 0.15 = -0.05. + // Integrated gap for unit square (overlap area 1.0) distributed to 4 nodes: + // Each node i has integrated gap G_i = \int \phi_i (g_kin - g_r) dA. + // For constant g_kin and g_r: G_i = (g_kin - g_r) \int \phi_i dA. + // \int \phi_i dA for bilinear quad node is 0.25 (1/4 of element area). + // So G_i = -0.05 * 0.25 = -0.0125. + + for ( int i = 0; i < 4; ++i ) { + EXPECT_NEAR( g1[i], -0.0125, 1e-12 ); + } +} + } // namespace tribol //------------------------------------------------------------------------------ diff --git a/src/tests/tribol_mfem_mortar_lm.cpp b/src/tests/tribol_mfem_mortar_lm.cpp index 98db248b..46f6f613 100644 --- a/src/tests/tribol_mfem_mortar_lm.cpp +++ b/src/tests/tribol_mfem_mortar_lm.cpp @@ -35,7 +35,7 @@ * @brief This tests the Tribol MFEM interface running a contact patch test. * */ -class MfemMortarTest : public testing::TestWithParam> { +class MfemMortarTest : public testing::TestWithParam> { protected: tribol::RealT max_disp_; void SetUp() override @@ -46,6 +46,8 @@ class MfemMortarTest : public testing::TestWithParam( GetParam() ); + // fixed options // boundary element attributes of mortar surface auto mortar_attrs = std::set( { 4 } ); @@ -144,6 +146,7 @@ class MfemMortarTest : public testing::TestWithParam( GetParam() ); + EXPECT_LT( std::abs( max_disp_ - ( 0.005 + residual_gap / 2.0 ) ), 1.0e-6 ); MPI_Barrier( MPI_COMM_WORLD ); } INSTANTIATE_TEST_SUITE_P( tribol, MfemMortarTest, - testing::Values( std::make_tuple( 2, mfem::Element::Type::HEXAHEDRON ), - std::make_tuple( 2, mfem::Element::Type::TETRAHEDRON ) ) ); + testing::Values( std::make_tuple( 2, mfem::Element::Type::HEXAHEDRON, 0.0 ), + std::make_tuple( 2, mfem::Element::Type::HEXAHEDRON, 0.01 ), + std::make_tuple( 2, mfem::Element::Type::TETRAHEDRON, 0.0 ), + std::make_tuple( 2, mfem::Element::Type::TETRAHEDRON, 0.01 ) ) ); //------------------------------------------------------------------------------ #include "axom/slic/core/SimpleLogger.hpp" diff --git a/src/tests/tribol_residual_gap.cpp b/src/tests/tribol_residual_gap.cpp new file mode 100644 index 00000000..5985b617 --- /dev/null +++ b/src/tests/tribol_residual_gap.cpp @@ -0,0 +1,179 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other Tribol Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: (MIT) + +// gtest includes +#include "gtest/gtest.h" + +// Tribol includes +#include "tribol/interface/tribol.hpp" +#include "tribol/common/Parameters.hpp" +#include "tribol/mesh/CouplingScheme.hpp" + +using RealT = tribol::RealT; + +class ResidualGapTest : public ::testing::Test { + protected: + void SetUp() override {} + void TearDown() override { tribol::finalize(); } +}; + +TEST_F( ResidualGapTest, common_plane_residual_gap ) +{ + // Setup two 3D quads with a 0.1 gap + // Quad 1: z = 0, x in [0, 1], y in [0, 1] + // Quad 2: z = 0.1, x in [0, 1], y in [0, 1] + + constexpr int numVerts = 4; + constexpr int numElems = 1; + RealT x1[numVerts] = { 0.0, 1.0, 1.0, 0.0 }; + RealT y1[numVerts] = { 0.0, 0.0, 1.0, 1.0 }; + RealT z1[numVerts] = { 0.0, 0.0, 0.0, 0.0 }; + + RealT x2[numVerts] = { 0.0, 1.0, 1.0, 0.0 }; + RealT y2[numVerts] = { 0.0, 0.0, 1.0, 1.0 }; + RealT z2[numVerts] = { 0.1, 0.1, 0.1, 0.1 }; + + // Connectivity for 3D quads + tribol::IndexT conn1[numVerts] = { 0, 1, 2, 3 }; + // Quad 2 normal points -z: 0-3-2-1 is CCW viewed from -z. + tribol::IndexT conn2[numVerts] = { 0, 3, 2, 1 }; + + tribol::registerMesh( 0, numElems, numVerts, &conn1[0], static_cast( tribol::LINEAR_QUAD ), &x1[0], &y1[0], + &z1[0], tribol::MemorySpace::Host ); + tribol::registerMesh( 1, numElems, numVerts, &conn2[0], static_cast( tribol::LINEAR_QUAD ), &x2[0], &y2[0], + &z2[0], tribol::MemorySpace::Host ); + + RealT fx1[numVerts] = { 0., 0., 0., 0. }; + RealT fy1[numVerts] = { 0., 0., 0., 0. }; + RealT fz1[numVerts] = { 0., 0., 0., 0. }; + RealT fx2[numVerts] = { 0., 0., 0., 0. }; + RealT fy2[numVerts] = { 0., 0., 0., 0. }; + RealT fz2[numVerts] = { 0., 0., 0., 0. }; + + tribol::registerNodalResponse( 0, &fx1[0], &fy1[0], &fz1[0] ); + tribol::registerNodalResponse( 1, &fx2[0], &fy2[0], &fz2[0] ); + + RealT penalty = 1.0; + tribol::setKinematicConstantPenalty( 0, penalty ); + tribol::setKinematicConstantPenalty( 1, penalty ); + + tribol::registerCouplingScheme( 0, 0, 1, tribol::SURFACE_TO_SURFACE, tribol::NO_CASE, tribol::COMMON_PLANE, + tribol::FRICTIONLESS, tribol::PENALTY, tribol::BINNING_GRID, + tribol::ExecutionMode::Sequential ); + + tribol::setPenaltyOptions( 0, tribol::KINEMATIC, tribol::KINEMATIC_CONSTANT ); + + // Default residual_gap is 0.0. Gap is 0.1, so no contact. + RealT dt = 1.0; + tribol::update( 1, 1.0, dt ); + + // there still should be an active pair even with no contact... + auto& cs = tribol::CouplingSchemeManager::getInstance().at( 0 ); + EXPECT_EQ( 1, cs.getNumActivePairs() ); + + // ...but the pair should have zero force + for ( int i = 0; i < numVerts; ++i ) { + EXPECT_NEAR( fz1[i], 0.0, 1.e-10 ); + EXPECT_NEAR( fz2[i], 0.0, 1.e-10 ); + } + + // Set residual_gap to 0.15. Now gap (0.1) < residual_gap (0.15), so contact. + RealT residual_gap = 0.15; + tribol::setResidualGap( 0, residual_gap ); + + tribol::update( 2, 2.0, dt ); + + EXPECT_EQ( 1, cs.getNumActivePairs() ); + + // Check forces. + // actual gap g = 0.1. + // pressure p = (g - residual_gap) * effective_penalty = (0.1 - 0.15) * 0.5 = -0.025 + // Force on mesh 1 nodes: repulsion points DOWN (-z). + // Total force = p * A = -0.025 * 1.0 = -0.025. + // Distributed to 4 nodes: -0.025 / 4 = -0.00625. + + for ( int i = 0; i < numVerts; ++i ) { + EXPECT_NEAR( fz1[i], -0.00625, 1.e-10 ); + EXPECT_NEAR( fz2[i], 0.00625, 1.e-10 ); + } +} + +TEST_F( ResidualGapTest, mortar_residual_gap ) +{ + // Setup two 3D quads with a 0.1 gap + constexpr int numVerts = 4; + constexpr int numElems = 1; + RealT x1[numVerts] = { 0.0, 1.0, 1.0, 0.0 }; + RealT y1[numVerts] = { 0.0, 0.0, 1.0, 1.0 }; + RealT z1[numVerts] = { 0.0, 0.0, 0.0, 0.0 }; + + RealT x2[numVerts] = { 0.0, 1.0, 1.0, 0.0 }; + RealT y2[numVerts] = { 0.0, 0.0, 1.0, 1.0 }; + RealT z2[numVerts] = { 0.1, 0.1, 0.1, 0.1 }; + + tribol::IndexT conn1[numVerts] = { 0, 1, 2, 3 }; + tribol::IndexT conn2[numVerts] = { 0, 3, 2, 1 }; + + tribol::registerMesh( 0, numElems, numVerts, &conn1[0], (int)( tribol::LINEAR_QUAD ), &x1[0], &y1[0], &z1[0], + tribol::MemorySpace::Host ); + tribol::registerMesh( 1, numElems, numVerts, &conn2[0], (int)( tribol::LINEAR_QUAD ), &x2[0], &y2[0], &z2[0], + tribol::MemorySpace::Host ); + + RealT fx1[numVerts] = { 0., 0., 0., 0. }; + RealT fy1[numVerts] = { 0., 0., 0., 0. }; + RealT fz1[numVerts] = { 0., 0., 0., 0. }; + RealT fx2[numVerts] = { 0., 0., 0., 0. }; + RealT fy2[numVerts] = { 0., 0., 0., 0. }; + RealT fz2[numVerts] = { 0., 0., 0., 0. }; + + tribol::registerNodalResponse( 0, &fx1[0], &fy1[0], &fz1[0] ); + tribol::registerNodalResponse( 1, &fx2[0], &fy2[0], &fz2[0] ); + + RealT gaps[numVerts] = { 0., 0., 0., 0. }; + RealT pressures[numVerts] = { 0., 0., 0., 0. }; + tribol::registerMortarGaps( 1, gaps ); + tribol::registerMortarPressures( 1, pressures ); + + tribol::registerCouplingScheme( 0, 0, 1, tribol::SURFACE_TO_SURFACE, tribol::NO_CASE, tribol::SINGLE_MORTAR, + tribol::FRICTIONLESS, tribol::LAGRANGE_MULTIPLIER, tribol::BINNING_GRID, + tribol::ExecutionMode::Sequential ); + + tribol::setLagrangeMultiplierOptions( 0, tribol::ImplicitEvalMode::MORTAR_RESIDUAL ); + + // Default residual_gap is 0.0. Gap is 0.1. + RealT dt = 1.0; + tribol::update( 1, 1.0, dt ); + + auto& cs = tribol::CouplingSchemeManager::getInstance().at( 0 ); + EXPECT_EQ( 1, cs.getNumActivePairs() ); + + // Nodal gaps in mortar are integrated: 0.1 * 0.25 = 0.025 + for ( int i = 0; i < numVerts; ++i ) { + EXPECT_NEAR( gaps[i], 0.025, 1.e-10 ); + } + + // Set residual_gap to 0.15. + RealT residual_gap = 0.15; + tribol::setResidualGap( 0, residual_gap ); + + // Clear gaps to avoid accumulation in test + for ( int i = 0; i < numVerts; ++i ) gaps[i] = 0.0; + + tribol::update( 2, 2.0, dt ); + + // Kinematic gap is (0.1 - 0.15) = -0.05. Integrated: -0.05 * 0.25 = -0.0125 + for ( int i = 0; i < numVerts; ++i ) { + EXPECT_NEAR( gaps[i], -0.0125, 1.e-10 ); + } +} + +int main( int argc, char* argv[] ) +{ + int result = 0; + ::testing::InitGoogleTest( &argc, argv ); + axom::slic::SimpleLogger logger; + result = RUN_ALL_TESTS(); + return result; +} diff --git a/src/tribol/common/Parameters.hpp b/src/tribol/common/Parameters.hpp index 2f5789d2..1061846b 100644 --- a/src/tribol/common/Parameters.hpp +++ b/src/tribol/common/Parameters.hpp @@ -494,6 +494,8 @@ struct Parameters { // constituent face elements, then we don't consider the face-pair a contact candidate. // Note, auto-contact will require registration of element thicknesses. bool auto_contact_check = false; ///! True if auto-contact checks should be enabled + + RealT residual_gap = 0.0; ///! User defined residual gap constraint }; } // namespace tribol diff --git a/src/tribol/geom/CompGeom.hpp b/src/tribol/geom/CompGeom.hpp index 80a2f460..d5657db1 100644 --- a/src/tribol/geom/CompGeom.hpp +++ b/src/tribol/geom/CompGeom.hpp @@ -1039,7 +1039,7 @@ TRIBOL_HOST_DEVICE inline bool CommonPlanePair::exceedsMaxAutoInterpen( const Me RealT max_interpen = -1. * params.auto_contact_pen_frac * axom::utilities::min( mesh1.getElementData().m_thickness[faceId1], mesh2.getElementData().m_thickness[faceId2] ); - if ( gap < max_interpen ) { + if ( ( gap - params.residual_gap ) < max_interpen ) { return true; } } diff --git a/src/tribol/interface/mfem_tribol.cpp b/src/tribol/interface/mfem_tribol.cpp index 3928150d..4feedaba 100644 --- a/src/tribol/interface/mfem_tribol.cpp +++ b/src/tribol/interface/mfem_tribol.cpp @@ -403,9 +403,11 @@ void updateMfemParallelDecomposition( int n_ranks, bool force_new_redecomp ) if ( mfem_data->GetLORFactor() > 1 ) { effective_binning_proximity *= static_cast( mfem_data->GetLORFactor() ); } + auto residual_gap = cs.getParameters().residual_gap; // creates a new redecomp mesh based on updated coordinates (if criteria is met) and updates transfer operators // and displacement, velocity, and response grid functions based on new redecomp mesh - auto new_redecomp = mfem_data->UpdateMfemMeshData( effective_binning_proximity, n_ranks, force_new_redecomp ); + auto new_redecomp = + mfem_data->UpdateMfemMeshData( effective_binning_proximity, n_ranks, force_new_redecomp, residual_gap ); auto coord_ptrs = mfem_data->GetRedecompCoordsPtrs(); registerMesh( mesh_ids[0], mfem_data->GetMesh1NE(), mfem_data->GetNV(), mfem_data->GetMesh1Conn(), diff --git a/src/tribol/interface/tribol.cpp b/src/tribol/interface/tribol.cpp index bad3cbe3..134a5aa2 100644 --- a/src/tribol/interface/tribol.cpp +++ b/src/tribol/interface/tribol.cpp @@ -337,6 +337,19 @@ void setBinningProximityScale( IndexT cs_id, RealT binning_proximity_scale ) } // end setBinningProximityScale() +//------------------------------------------------------------------------------ +void setResidualGap( IndexT cs_id, RealT residual_gap ) +{ + auto cs = CouplingSchemeManager::getInstance().findData( cs_id ); + + // check to see if coupling scheme exists + SLIC_ERROR_ROOT_IF( + !cs, "tribol::setResidualGap(): call tribol::registerCouplingScheme() " << "prior to calling this routine." ); + + cs->getParameters().residual_gap = residual_gap; + +} // end setResidualGap() + //------------------------------------------------------------------------------ void enableTimestepVote( IndexT cs_id, const bool enable ) { diff --git a/src/tribol/interface/tribol.hpp b/src/tribol/interface/tribol.hpp index 36e6fa03..0f83f528 100644 --- a/src/tribol/interface/tribol.hpp +++ b/src/tribol/interface/tribol.hpp @@ -215,6 +215,15 @@ void setLoggingLevel( IndexT cs_id, LoggingLevel log_level ); */ void setBinningProximityScale( IndexT cs_id, RealT binning_proximity_scale ); +/*! + * @brief Sets the residual gap for a coupling scheme + * + * @param [in] cs_id coupling scheme id + * @param [in] residual_gap the gap offset. Positive values shift the contact surface away from the mesh + * surface, making contact occur earlier (with a gap). Effective gap = kinematic gap - residual gap. + */ +void setResidualGap( IndexT cs_id, RealT residual_gap ); + /*! * \brief Enable the contact timestep vote * diff --git a/src/tribol/mesh/CouplingScheme.hpp b/src/tribol/mesh/CouplingScheme.hpp index abc16d5f..a6c008e3 100644 --- a/src/tribol/mesh/CouplingScheme.hpp +++ b/src/tribol/mesh/CouplingScheme.hpp @@ -1018,8 +1018,9 @@ TRIBOL_HOST_DEVICE inline RealT CouplingScheme::Viewer::getGapTol( int fid1, int break; default: - gap_tol = -1. * m_parameters.gap_tol_ratio * - axom::utilities::max( m_mesh1.getFaceRadius()[fid1], m_mesh2.getFaceRadius()[fid2] ); + gap_tol = m_parameters.residual_gap - + m_parameters.gap_tol_ratio * + axom::utilities::max( m_mesh1.getFaceRadius()[fid1], m_mesh2.getFaceRadius()[fid2] ); break; } // end switch over m_contactModel diff --git a/src/tribol/mesh/MfemData.cpp b/src/tribol/mesh/MfemData.cpp index e5a10747..6723459b 100644 --- a/src/tribol/mesh/MfemData.cpp +++ b/src/tribol/mesh/MfemData.cpp @@ -334,7 +334,8 @@ void MfemMeshData::SetParentReferenceCoords( const mfem::ParGridFunction& refere } } -bool MfemMeshData::UpdateMfemMeshData( RealT binning_proximity_scale, int n_ranks, bool force_new_redecomp ) +bool MfemMeshData::UpdateMfemMeshData( RealT binning_proximity_scale, int n_ranks, bool force_new_redecomp, + RealT residual_gap ) { TRIBOL_MARK_FUNCTION; @@ -385,10 +386,10 @@ bool MfemMeshData::UpdateMfemMeshData( RealT binning_proximity_scale, int n_rank submesh_lor_xfer_->SubmeshToLOR( *submesh_nodes, *lor_nodes ); TRIBOL_MARK_END( "Update LOR coords" ); } - update_data_ = - std::make_unique( submesh_, lor_mesh_.get(), *coords_.GetParentGridFn().ParFESpace(), - submesh_xfer_gridfn_, submesh_lor_xfer_.get(), attributes_1_, attributes_2_, - binning_proximity_scale, n_ranks, allocator_id_, redecomp_trigger_displacement_ ); + update_data_ = std::make_unique( submesh_, lor_mesh_.get(), *coords_.GetParentGridFn().ParFESpace(), + submesh_xfer_gridfn_, submesh_lor_xfer_.get(), attributes_1_, + attributes_2_, binning_proximity_scale, n_ranks, allocator_id_, + redecomp_trigger_displacement_, residual_gap ); rebuilt = true; } @@ -630,19 +631,19 @@ MfemMeshData::UpdateData::UpdateData( mfem::ParSubMesh& submesh, mfem::ParMesh* mfem::ParGridFunction& submesh_gridfn, SubmeshLORTransfer* submesh_lor_xfer, const std::set& attributes_1, const std::set& attributes_2, RealT binning_proximity_scale, int n_ranks, int allocator_id, - RealT redecomp_trigger_displacement ) + RealT redecomp_trigger_displacement, RealT residual_gap ) : redecomp_mesh_{ lor_mesh ? redecomp::RedecompMesh( *lor_mesh, binning_proximity_scale * redecomp::RedecompMesh::MaxElementSize( *lor_mesh, redecomp::MPIUtility( lor_mesh->GetComm() ) ) + - redecomp_trigger_displacement, + redecomp_trigger_displacement + residual_gap, redecomp::RedecompMesh::RCB, n_ranks ) : redecomp::RedecompMesh( submesh, binning_proximity_scale * redecomp::RedecompMesh::MaxElementSize( submesh, redecomp::MPIUtility( submesh.GetComm() ) ) + - redecomp_trigger_displacement, + redecomp_trigger_displacement + residual_gap, redecomp::RedecompMesh::RCB, n_ranks ) }, vector_xfer_{ parent_fes, submesh_gridfn, submesh_lor_xfer, redecomp_mesh_ }, allocator_id_{ allocator_id } diff --git a/src/tribol/mesh/MfemData.hpp b/src/tribol/mesh/MfemData.hpp index e044ccb0..63177d12 100644 --- a/src/tribol/mesh/MfemData.hpp +++ b/src/tribol/mesh/MfemData.hpp @@ -647,11 +647,13 @@ class MfemMeshData { * @param n_ranks Number of ranks in the parallel decomposition * @param force_new_redecomp If true, construct a new RedecompMesh even if displacement threshold is not met (default * = false) + * @param residual_gap Residual gap to add to ghost layer thickness (default = 0.0) * @return True if a new RedecompMesh is created by this method * * @note This method should be called after the coordinate grid function is updated. */ - bool UpdateMfemMeshData( RealT binning_proximity_scale, int n_ranks, bool force_new_redecomp = false ); + bool UpdateMfemMeshData( RealT binning_proximity_scale, int n_ranks, bool force_new_redecomp = false, + RealT residual_gap = 0.0 ); /** * @brief Get the integer identifier for the first Tribol registered mesh @@ -1127,11 +1129,12 @@ class MfemMeshData { * @param allocator_id Allocation space ID for Tribol memory * @param redecomp_trigger_displacement Additional length to add to redecomp ghost length equal to the * displacement required to trigger a new RedecompMesh to be built. + * @param residual_gap user-defined residual gap */ UpdateData( mfem::ParSubMesh& submesh, mfem::ParMesh* lor_mesh, const mfem::ParFiniteElementSpace& parent_fes, mfem::ParGridFunction& submesh_gridfn, SubmeshLORTransfer* submesh_lor_xfer, const std::set& attributes_1, const std::set& attributes_2, RealT binning_proximity_scale, - int n_ranks, int allocator_id, RealT redecomp_trigger_displacement ); + int n_ranks, int allocator_id, RealT redecomp_trigger_displacement, RealT residual_gap ); /** * @brief Redecomposed boundary element mesh diff --git a/src/tribol/physics/AlignedMortar.cpp b/src/tribol/physics/AlignedMortar.cpp index a9aac9ec..5e5f471c 100644 --- a/src/tribol/physics/AlignedMortar.cpp +++ b/src/tribol/physics/AlignedMortar.cpp @@ -70,7 +70,7 @@ void ComputeAlignedMortarWeights( SurfaceContactElem& elem ) //------------------------------------------------------------------------------ template <> -void ComputeNodalGap( SurfaceContactElem& elem ) +void ComputeNodalGap( SurfaceContactElem& elem, RealT residual_gap ) { // get pointer to mesh view to store gaps on mesh data object auto& nonmortarMesh = *elem.m_mesh2; @@ -135,7 +135,7 @@ void ComputeNodalGap( SurfaceContactElem& elem ) v[1] = elem.faceCoords1[elem.dim * mortarNodeId + 1] - elem.faceCoords2[elem.dim * a + 1]; v[2] = elem.faceCoords1[elem.dim * mortarNodeId + 2] - elem.faceCoords2[elem.dim * a + 2]; - nonmortarMesh.getNodalFields().m_node_gap[glbId] += dotProd( &v[0], &nrml_a[0], elem.dim ); + nonmortarMesh.getNodalFields().m_node_gap[glbId] += dotProd( &v[0], &nrml_a[0], elem.dim ) - residual_gap; } } // end of ComputeNodalGap<>() @@ -168,6 +168,8 @@ void ComputeAlignedMortarGaps( CouplingScheme* cs ) Array2D mortarX( numNodesPerFace, dim ); Array2D nonmortarX( numNodesPerFace, dim ); + RealT residual_gap = cs->getParameters().residual_gap; + //////////////////////////// // compute nonmortar gaps // //////////////////////////// @@ -209,7 +211,7 @@ void ComputeAlignedMortarGaps( CouplingScheme* cs ) ///////////////////////// // compute mortar gaps // ///////////////////////// - ComputeNodalGap( elem_for_gap ); + ComputeNodalGap( elem_for_gap, residual_gap ); // HAVE TO set the number of active constraints. For now set to // all nonmortar face nodes. diff --git a/src/tribol/physics/AlignedMortar.hpp b/src/tribol/physics/AlignedMortar.hpp index c9998149..12ec72fd 100644 --- a/src/tribol/physics/AlignedMortar.hpp +++ b/src/tribol/physics/AlignedMortar.hpp @@ -43,23 +43,14 @@ void ComputeAlignedMortarWeights( SurfaceContactElem& elem ); * * \brief compute a contact element's contribution to nodal gaps * - * \param [in] cs pointer to the coupling scheme - * - */ -template -void ComputeNodalGap( SurfaceContactElem& elem ); - -/*! + * \note explicit specialization for aligned mortar method * - * \brief compute a contact element's contribution to nodal gaps - * - * \note explicit specialization for single mortar method - * - * \param [in] cs pointer to the coupling scheme + * \param [in] elem surface contact element object for contact face-pair + * \param [in] residual_gap user-defined residual gap * */ template <> -void ComputeNodalGap( SurfaceContactElem& elem ); +void ComputeNodalGap( SurfaceContactElem& elem, RealT residual_gap ); /*! * diff --git a/src/tribol/physics/CommonPlane.cpp b/src/tribol/physics/CommonPlane.cpp index 39eae36a..f5973718 100644 --- a/src/tribol/physics/CommonPlane.cpp +++ b/src/tribol/physics/CommonPlane.cpp @@ -205,7 +205,8 @@ int ApplyNormal( CouplingScheme* cs ) // compute total pressure based on constraint type RealT totalPressure = 0.; - plane.m_pressure = gap * penalty_stiff_per_area; // kinematic contribution + RealT residual_gap = cs_view.getParameters().residual_gap; + plane.m_pressure = ( gap - residual_gap ) * penalty_stiff_per_area; // kinematic contribution switch ( pen_enfrc_options.constraint_type ) { case KINEMATIC_AND_RATE: { // kinematic contribution diff --git a/src/tribol/physics/Mortar.cpp b/src/tribol/physics/Mortar.cpp index 91e85f1d..ee1e1640 100644 --- a/src/tribol/physics/Mortar.cpp +++ b/src/tribol/physics/Mortar.cpp @@ -113,7 +113,7 @@ void ComputeMortarWeights( SurfaceContactElem& elem ) //------------------------------------------------------------------------------ template <> -void ComputeNodalGap( SurfaceContactElem& elem ) +void ComputeNodalGap( SurfaceContactElem& elem, RealT residual_gap ) { // check to make sure mortar weights have been computed locally // for the SurfaceContactElem object @@ -161,6 +161,9 @@ void ComputeNodalGap( SurfaceContactElem& elem ) g1 += dotProd( nrml_a.data(), &elem.faceCoords1[elem.dim * b], elem.dim ) * nab_1; g2 += dotProd( nrml_a.data(), &elem.faceCoords2[elem.dim * b], elem.dim ) * nab_2; + + // also integrate the residual gap contribution + g2 += residual_gap * nab_2; } // store local gap @@ -200,6 +203,8 @@ void ComputeSingleMortarGaps( CouplingScheme* cs ) Array2D mortarX_bar( numNodesPerFace, dim ); Array2D nonmortarX_bar( numNodesPerFace, dim ); + RealT residual_gap = cs->getParameters().residual_gap; + //////////////////////////////////////////////////////////////////// // compute nonmortar gaps to determine active set of contact dofs // //////////////////////////////////////////////////////////////////// @@ -247,7 +252,7 @@ void ComputeSingleMortarGaps( CouplingScheme* cs ) elem.faceCoords1 = mortarX.data(); elem.faceCoords2 = nonmortarX.data(); - ComputeNodalGap( elem ); + ComputeNodalGap( elem, residual_gap ); // TODO: fix this to register the actual number of active nonmortar gaps. // This is not the appropriate data structure to put this information in @@ -693,7 +698,8 @@ int ApplyNormalEnzyme( CouplingScheme* cs ) ComputeMortarJacobianEnzyme( x1, n1, p1, f1, blockJ( 0, 0 ).data(), blockJ( 0, 1 ).data(), blockJ_n( 0, 0 ).data(), blockJ( 0, 2 ).data(), g1, blockJ( 2, 0 ).data(), blockJ( 2, 1 ).data(), blockJ_n( 2, 0 ).data(), size1, x2, f2, blockJ( 1, 0 ).data(), - blockJ( 1, 1 ).data(), blockJ_n( 1, 0 ).data(), blockJ( 1, 2 ).data(), size2 ); + blockJ( 1, 1 ).data(), blockJ_n( 1, 0 ).data(), blockJ( 1, 2 ).data(), size2, + cs->getParameters().residual_gap ); if ( lm_opts.sparse_mode == SparseMode::MFEM_ELEMENT_DENSE ) { cs->getMethodData()->storeElemBlockJ( { elem1, elem2, elem1 }, blockJ ); @@ -704,7 +710,7 @@ int ApplyNormalEnzyme( CouplingScheme* cs ) } } else if ( lm_opts.eval_mode == ImplicitEvalMode::MORTAR_GAP || lm_opts.eval_mode == ImplicitEvalMode::MORTAR_RESIDUAL ) { - ComputeMortarForceEnzyme( x1, n1, p1, f1, g1, size1, x2, f2, size2 ); + ComputeMortarForceEnzyme( x1, n1, p1, f1, g1, size1, x2, f2, size2, cs->getParameters().residual_gap ); } for ( int i{ 0 }; i < size1; ++i ) { int node_id = mesh1.getGlobalNodeId( elem1, i ); @@ -726,7 +732,7 @@ int ApplyNormalEnzyme( CouplingScheme* cs ) //------------------------------------------------------------------------------ void ComputeMortarForceEnzyme( const RealT* x1, const RealT* n1, const RealT* p1, RealT* f1, RealT* g1, int size1, - const RealT* x2, RealT* f2, int size2 ) + const RealT* x2, RealT* f2, int size2, RealT residual_gap ) { // convention: elem1 = nonmortar element // elem2 = mortar element @@ -973,6 +979,9 @@ void ComputeMortarForceEnzyme( const RealT* x1, const RealT* n1, const RealT* p1 for ( int d{ 0 }; d < 3; ++d ) { g1[i] += n1[d * size1 + i] * gap_v[d]; } + for ( int j{ 0 }; j < size1; ++j ) { + g1[i] -= residual_gap * mortar_mat1[i * size1 + j]; + } } // compute nonmortar force contributions @@ -1004,7 +1013,7 @@ void ComputeMortarForceEnzyme( const RealT* x1, const RealT* n1, const RealT* p1 void ComputeMortarJacobianEnzyme( const RealT* x1, const RealT* n1, const RealT* p1, RealT* f1, RealT* df1dx1, RealT* df1dx2, RealT* df1dn1, RealT* df1dp1, RealT* g1, RealT* dg1dx1, RealT* dg1dx2, RealT* dg1dn1, int size1, const RealT* x2, RealT* f2, RealT* df2dx1, RealT* df2dx2, - RealT* df2dn1, RealT* df2dp1, int size2 ) + RealT* df2dn1, RealT* df2dp1, int size2, RealT residual_gap ) { RealT x1_dot[12] = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; for ( int i{ 0 }; i < size1 * 3; ++i ) { @@ -1019,7 +1028,8 @@ void ComputeMortarJacobianEnzyme( const RealT* x1, const RealT* n1, const RealT* TRIBOL_ENZYME_CONST, size1, TRIBOL_ENZYME_CONST, x2, TRIBOL_ENZYME_DUP, f2, &df2dx1[size1*3*i], - TRIBOL_ENZYME_CONST, size2); + TRIBOL_ENZYME_CONST, size2, + TRIBOL_ENZYME_CONST, residual_gap); // clang-format on x1_dot[i] = 0.0; } @@ -1036,7 +1046,8 @@ void ComputeMortarJacobianEnzyme( const RealT* x1, const RealT* n1, const RealT* TRIBOL_ENZYME_CONST, size1, TRIBOL_ENZYME_CONST, x2, TRIBOL_ENZYME_DUP, f2, &df2dn1[size1*3*i], - TRIBOL_ENZYME_CONST, size2); + TRIBOL_ENZYME_CONST, size2, + TRIBOL_ENZYME_CONST, residual_gap); // clang-format on n1_dot[i] = 0.0; } @@ -1053,7 +1064,8 @@ void ComputeMortarJacobianEnzyme( const RealT* x1, const RealT* n1, const RealT* TRIBOL_ENZYME_CONST, size1, TRIBOL_ENZYME_CONST, x2, TRIBOL_ENZYME_DUP, f2, &df2dp1[size1*3*i], - TRIBOL_ENZYME_CONST, size2); + TRIBOL_ENZYME_CONST, size2, + TRIBOL_ENZYME_CONST, residual_gap); // clang-format on p1_dot[i] = 0.0; } @@ -1070,7 +1082,8 @@ void ComputeMortarJacobianEnzyme( const RealT* x1, const RealT* n1, const RealT* TRIBOL_ENZYME_CONST, size1, TRIBOL_ENZYME_DUP, x2, x2_dot, TRIBOL_ENZYME_DUP, f2, &df2dx2[size2*3*i], - TRIBOL_ENZYME_CONST, size2); + TRIBOL_ENZYME_CONST, size2, + TRIBOL_ENZYME_CONST, residual_gap); // clang-format on x2_dot[i] = 0.0; } diff --git a/src/tribol/physics/Mortar.hpp b/src/tribol/physics/Mortar.hpp index efec3a9f..559eb5c5 100644 --- a/src/tribol/physics/Mortar.hpp +++ b/src/tribol/physics/Mortar.hpp @@ -49,10 +49,11 @@ void ComputeSingleMortarGaps( CouplingScheme* cs ); * \brief compute a contact element's contribution to nodal gaps * * \param [in] elem surface contact element object for contact face-pair + * \param [in] residual_gap user-defined residual gap * */ template -void ComputeNodalGap( SurfaceContactElem& elem ); +void ComputeNodalGap( SurfaceContactElem& elem, RealT residual_gap = 0. ); /*! * @@ -61,10 +62,11 @@ void ComputeNodalGap( SurfaceContactElem& elem ); * \note explicit specialization for single mortar method * * \param [in] elem surface contact element object for contact face-pair + * \param [in] residual_gap user-defined residual gap * */ template <> -void ComputeNodalGap( SurfaceContactElem& elem ); +void ComputeNodalGap( SurfaceContactElem& elem, RealT residual_gap ); /*! * @@ -185,9 +187,10 @@ int ApplyNormalEnzyme( CouplingScheme* cs ); * z3]) * @param [out] f2 Nodal forces for element 2 (stored by node, e.g. [x0, x1, x2, x3, y0, y1, y2, y3, z0, z1, z2, z3]) * @param [in] size2 Number of nodes on element 2 + * @param [in] residual_gap user-defined residual gap */ void ComputeMortarForceEnzyme( const RealT* x1, const RealT* n1, const RealT* p1, RealT* f1, RealT* g1, int size1, - const RealT* x2, RealT* f2, int size2 ); + const RealT* x2, RealT* f2, int size2, RealT residual_gap = 0.0 ); /** * @brief Computes the frictionless mortar forces and Jacobian for a 3D quad element (following Puso and Laursen (2004)) @@ -226,11 +229,12 @@ void ComputeMortarForceEnzyme( const RealT* x1, const RealT* n1, const RealT* p1 * @param [out] df2dp1 Derivative of element 2 nodal forces with respect to element 1 nodal pressures (size = * num_nodes_per_elem^2 x spatial dim) * @param [in] size2 Number of nodes on element 2 + * @param [in] residual_gap user-defined residual gap */ void ComputeMortarJacobianEnzyme( const RealT* x1, const RealT* n1, const RealT* p1, RealT* f1, RealT* df1dx1, RealT* df1dx2, RealT* df1dn1, RealT* df1dp1, RealT* g1, RealT* dg1dx1, RealT* dg1dx2, RealT* dg1dn1, int size1, const RealT* x2, RealT* f2, RealT* df2dx1, RealT* df2dx2, - RealT* df2dn1, RealT* df2dp1, int size2 ); + RealT* df2dn1, RealT* df2dp1, int size2, RealT residual_gap = 0.0 ); #endif /*! diff --git a/src/tribol/search/InterfacePairFinder.cpp b/src/tribol/search/InterfacePairFinder.cpp index 40b7fb09..fb6a1996 100644 --- a/src/tribol/search/InterfacePairFinder.cpp +++ b/src/tribol/search/InterfacePairFinder.cpp @@ -87,7 +87,6 @@ class CartesianProduct : public SearchBase { if ( is_symm ) { // account for symmetry: the max number of pairs when the meshes are the // same is the upper triangular portion of the cartesian product pair - // matrix maxNumPairs = mesh1NumElems * ( mesh1NumElems + 1 ) / 2; } ArrayT proximityArray( maxNumPairs, maxNumPairs, m_coupling_scheme->getAllocatorId() ); @@ -236,9 +235,10 @@ class GridSearch : public SearchBase { // * Find the average extents (range) of the boxes Assumption is that elements are roughly the same size // * Grid resolution for each dimension is overall box width divided by half the average element width SpaceVec ranges; + RealT residual_gap = m_coupling_scheme->getParameters().residual_gap; for ( int i = 0; i < m_mesh1.numberOfElements(); ++i ) { auto& bbox = m_meshBBoxes1[i]; - inflateBBox( bbox, e_binning_proximity_scale ); + inflateBBox( bbox, e_binning_proximity_scale, residual_gap ); ranges += bbox.range(); @@ -299,9 +299,10 @@ class GridSearch : public SearchBase { // Find matches in first mesh (with index 'fromIdx') // with candidate elements in second mesh (with index 'toIdx') // int k = 0; // Debug only + RealT residual_gap = m_coupling_scheme->getParameters().residual_gap; for ( int toIdx = 0; toIdx < m_mesh2.numberOfElements(); ++toIdx ) { SpatialBoundingBox bbox = elementBoundingBox( m_mesh2, toIdx ); - inflateBBox( bbox, e_binning_proximity_scale ); + inflateBBox( bbox, e_binning_proximity_scale, residual_gap ); // Query the mesh auto candidateBits = m_grid.getCandidates( bbox ); @@ -352,12 +353,12 @@ class GridSearch : public SearchBase { return box; } /*! - * Expands bounding box by range_multiplier * the longest dimension's range + * Expands bounding box by range_multiplier * the longest dimension's range + residual gap */ - void inflateBBox( SpatialBoundingBox& bbox, RealT range_multiplier ) + void inflateBBox( SpatialBoundingBox& bbox, RealT range_multiplier, RealT residual_gap = 0.0 ) { int d = bbox.getLongestDimension(); - const RealT expansionFac = range_multiplier * bbox.range()[d]; + const RealT expansionFac = range_multiplier * bbox.range()[d] + residual_gap; bbox.expand( expansionFac ); } @@ -421,11 +422,11 @@ class BvhSearch : public SearchBase { void initialize() override { // we want binning proximity scaled by LOR factor on HO meshes, i.e. the effective binning proximity - buildMeshBBoxes( m_boxes1, m_coupling_scheme->getMesh1().getView(), - m_coupling_scheme->getEffectiveBinningProximityScale() ); + auto e_binning_proximity_scale = m_coupling_scheme->getEffectiveBinningProximityScale(); + auto residual_gap = m_coupling_scheme->getParameters().residual_gap; + buildMeshBBoxes( m_boxes1, m_coupling_scheme->getMesh1().getView(), e_binning_proximity_scale, residual_gap ); // we want binning proximity scaled by LOR factor on HO meshes, i.e. the effective binning proximity - buildMeshBBoxes( m_boxes2, m_coupling_scheme->getMesh2().getView(), - m_coupling_scheme->getEffectiveBinningProximityScale() ); + buildMeshBBoxes( m_boxes2, m_coupling_scheme->getMesh2().getView(), e_binning_proximity_scale, residual_gap ); } // end initialize() /*! @@ -498,11 +499,11 @@ class BvhSearch : public SearchBase { } ); } // end findInterfacePairs() - void buildMeshBBoxes( ArrayT& boxes, const MeshData::Viewer& mesh, RealT binning_proximity ) + void buildMeshBBoxes( ArrayT& boxes, const MeshData::Viewer& mesh, RealT binning_proximity, RealT residual_gap ) { auto boxes_view = boxes.view(); forAllExec( m_coupling_scheme->getExecutionMode(), mesh.numberOfElements(), - [this, mesh, boxes_view, binning_proximity] TRIBOL_HOST_DEVICE( IndexT i ) { + [this, mesh, boxes_view, binning_proximity, residual_gap] TRIBOL_HOST_DEVICE( IndexT i ) { BoxT box; auto num_nodes_per_elem = mesh.numberOfNodesPerElement(); for ( IndexT j{ 0 }; j < num_nodes_per_elem; ++j ) { @@ -518,7 +519,7 @@ class BvhSearch : public SearchBase { mesh.getFaceNormal( i, vnorm ); VectorT faceNormal( vnorm ); RealT faceRadius = mesh.getFaceRadius()[i]; - expandBBoxNormal( box, faceNormal, binning_proximity * faceRadius ); + expandBBoxNormal( box, faceNormal, binning_proximity * faceRadius + residual_gap ); boxes_view[i] = std::move( box ); } ); } diff --git a/src/tribol/search/InterfacePairFinder.hpp b/src/tribol/search/InterfacePairFinder.hpp index 8a028b94..bbb625ec 100644 --- a/src/tribol/search/InterfacePairFinder.hpp +++ b/src/tribol/search/InterfacePairFinder.hpp @@ -34,6 +34,7 @@ TRIBOL_HOST_DEVICE inline bool geomFilter( const CouplingScheme::Viewer& cs_view // we want binning proximity scaled by LOR factor on HO meshes, i.e. the effective binning proximity auto element_radius_multiplier = cs_view.getEffectiveBinningProximityScale(); auto mode = cs_view.getContactMode(); + RealT residual_gap = cs_view.getParameters().residual_gap; /// CHECK #1: Check to make sure the two face ids are not the same /// and the two mesh ids are not the same. @@ -91,14 +92,14 @@ TRIBOL_HOST_DEVICE inline bool geomFilter( const CouplingScheme::Viewer& cs_view RealT r2 = mesh2.getFaceRadius()[element_id2]; // set maximum offset of face centroids for inclusion - RealT distMax = element_radius_multiplier * ( r1 + r2 ); // default is sum of face radii + RealT distMax = element_radius_multiplier * ( r1 + r2 ) + residual_gap; // default is sum of face radii // check if the contact mode is conforming, in which case the // faces are supposed to be aligned if ( mode == SURFACE_TO_SURFACE_CONFORMING ) { // use 5% of max face radius for conforming case as // tolerance on face offsets - distMax *= offset_tol; + distMax = offset_tol * element_radius_multiplier * ( r1 + r2 ) + residual_gap; } // compute the distance between the two face centroids @@ -117,14 +118,14 @@ TRIBOL_HOST_DEVICE inline bool geomFilter( const CouplingScheme::Viewer& cs_view RealT e1 = 0.5 * mesh1.getElementAreas()[element_id1]; RealT e2 = 0.5 * mesh2.getElementAreas()[element_id2]; - RealT distMax = element_radius_multiplier * ( e1 + e2 ); + RealT distMax = element_radius_multiplier * ( e1 + e2 ) + residual_gap; // check if the contact mode is conforming, in which case the // edges are supposed to be aligned if ( mode == SURFACE_TO_SURFACE_CONFORMING ) { // use 5% of max face radius for conforming case as // tolerance on face offsets - distMax *= offset_tol; + distMax = offset_tol * element_radius_multiplier * ( e1 + e2 ) + residual_gap; } // compute the distance between the two edge centroids