Serac  0.1
Serac is an implicit thermal strucural mechanics simulation code.
contact_data.cpp
1 // Copyright (c) 2019-2024, Lawrence Livermore National Security, LLC and
2 // other Serac Project Developers. See the top-level LICENSE file for
3 // details.
4 //
5 // SPDX-License-Identifier: (BSD-3-Clause)
6 
8 
9 #include "axom/slic.hpp"
10 
11 #ifdef SERAC_USE_TRIBOL
12 #include "tribol/interface/tribol.hpp"
13 #include "tribol/interface/mfem_tribol.hpp"
14 #endif
15 
16 namespace serac {
17 
18 #ifdef SERAC_USE_TRIBOL
19 
20 ContactData::ContactData(const mfem::ParMesh& mesh)
21  : mesh_{mesh},
22  reference_nodes_{dynamic_cast<const mfem::ParGridFunction*>(mesh.GetNodes())},
23  current_coords_{*reference_nodes_},
24  have_lagrange_multipliers_{false},
25  num_pressure_dofs_{0},
26  offsets_up_to_date_{false}
27 {
28  tribol::initialize(mesh_.SpaceDimension(), mesh_.GetComm());
29 }
30 
31 ContactData::~ContactData() { tribol::finalize(); }
32 
33 void ContactData::addContactInteraction(int interaction_id, const std::set<int>& bdry_attr_surf1,
34  const std::set<int>& bdry_attr_surf2, ContactOptions contact_opts)
35 {
36  interactions_.emplace_back(interaction_id, mesh_, bdry_attr_surf1, bdry_attr_surf2, current_coords_, contact_opts);
37  if (contact_opts.enforcement == ContactEnforcement::LagrangeMultiplier) {
38  have_lagrange_multipliers_ = true;
39  num_pressure_dofs_ += interactions_.back().numPressureDofs();
40  offsets_up_to_date_ = false;
41  }
42 }
43 
44 void ContactData::update(int cycle, double time, double& dt)
45 {
46  // This updates the redecomposed surface mesh based on the current displacement, then transfers field quantities to
47  // the updated mesh.
48  tribol::updateMfemParallelDecomposition();
49  // This function computes forces, gaps, and Jacobian contributions based on the current field quantities. Note the
50  // fields (with the exception of pressure) are stored on the redecomposed surface mesh until transferred by calling
51  // forces(), mergedGaps(), etc.
52  tribol::update(cycle, time, dt);
53 }
54 
55 FiniteElementDual ContactData::forces() const
56 {
57  FiniteElementDual f(*reference_nodes_->ParFESpace(), "contact force");
58  for (const auto& interaction : interactions_) {
59  f += interaction.forces();
60  }
61  return f;
62 }
63 
64 mfem::HypreParVector ContactData::mergedPressures() const
65 {
66  updateDofOffsets();
67  mfem::HypreParVector merged_p(mesh_.GetComm(), global_pressure_dof_offsets_[global_pressure_dof_offsets_.Size() - 1],
68  global_pressure_dof_offsets_.GetData());
69  for (size_t i{0}; i < interactions_.size(); ++i) {
70  if (interactions_[i].getContactOptions().enforcement == ContactEnforcement::LagrangeMultiplier) {
71  mfem::Vector p_interaction;
72  p_interaction.MakeRef(
73  merged_p, pressure_dof_offsets_[static_cast<int>(i)],
74  pressure_dof_offsets_[static_cast<int>(i) + 1] - pressure_dof_offsets_[static_cast<int>(i)]);
75  p_interaction.Set(1.0, interactions_[i].pressure());
76  }
77  }
78  return merged_p;
79 }
80 
81 mfem::HypreParVector ContactData::mergedGaps(bool zero_inactive) const
82 {
83  updateDofOffsets();
84  mfem::HypreParVector merged_g(mesh_.GetComm(), global_pressure_dof_offsets_[global_pressure_dof_offsets_.Size() - 1],
85  global_pressure_dof_offsets_.GetData());
86  for (size_t i{0}; i < interactions_.size(); ++i) {
87  if (interactions_[i].getContactOptions().enforcement == ContactEnforcement::LagrangeMultiplier) {
88  auto g = interactions_[i].gaps();
89  if (zero_inactive) {
90  for (auto dof : interactions_[i].inactiveDofs()) {
91  g[dof] = 0.0;
92  }
93  }
94  mfem::Vector g_interaction(
95  merged_g, pressure_dof_offsets_[static_cast<int>(i)],
96  pressure_dof_offsets_[static_cast<int>(i) + 1] - pressure_dof_offsets_[static_cast<int>(i)]);
97  g_interaction.Set(1.0, g);
98  }
99  }
100  return merged_g;
101 }
102 
103 std::unique_ptr<mfem::BlockOperator> ContactData::mergedJacobian() const
104 {
105  updateDofOffsets();
106  // this is the BlockOperator we are returning with the following blocks:
107  // | df_(contact)/dx df_(contact)/dp |
108  // | dg/dx I_(inactive) |
109  // where I_(inactive) is a matrix with ones on the diagonal of inactive pressure true degrees of freedom
110  auto block_J = std::make_unique<mfem::BlockOperator>(jacobian_offsets_);
111  block_J->owns_blocks = true;
112  // rather than returning different blocks for each contact interaction with Lagrange multipliers, merge them all into
113  // a single block
114  mfem::Array2D<const mfem::HypreParMatrix*> constraint_matrices(static_cast<int>(interactions_.size()), 1);
115 
116  for (size_t i{0}; i < interactions_.size(); ++i) {
117  // this is the BlockOperator for one of the contact interactions
118  auto interaction_J = interactions_[i].jacobian();
119  interaction_J->owns_blocks = false; // we'll manage the ownership of the blocks on our own...
120  // add the contact interaction's contribution to df_(contact)/dx (the 0, 0 block)
121  if (!interaction_J->IsZeroBlock(0, 0)) {
122  SLIC_ERROR_ROOT_IF(!dynamic_cast<mfem::HypreParMatrix*>(&interaction_J->GetBlock(0, 0)),
123  "Only HypreParMatrix constraint matrix blocks are currently supported.");
124  if (block_J->IsZeroBlock(0, 0)) {
125  block_J->SetBlock(0, 0, &interaction_J->GetBlock(0, 0));
126  } else {
127  if (block_J->IsZeroBlock(0, 0)) {
128  block_J->SetBlock(0, 0, &interaction_J->GetBlock(0, 0));
129  } else {
130  block_J->SetBlock(0, 0,
131  mfem::Add(1.0, static_cast<mfem::HypreParMatrix&>(block_J->GetBlock(0, 0)), 1.0,
132  static_cast<mfem::HypreParMatrix&>(interaction_J->GetBlock(0, 0))));
133  }
134  delete &interaction_J->GetBlock(0, 0);
135  }
136  }
137  // add the contact interaction's (other) contribution to df_(contact)/dx (for penalty) or to df_(contact)/dp and
138  // dg/dx (for Lagrange multipliers)
139  if (!interaction_J->IsZeroBlock(1, 0)) {
140  auto B = dynamic_cast<mfem::HypreParMatrix*>(&interaction_J->GetBlock(1, 0));
141  SLIC_ERROR_ROOT_IF(!B, "Only HypreParMatrix constraint matrix blocks are currently supported.");
142  // zero out rows not in the active set
143  B->EliminateRows(interactions_[i].inactiveDofs());
144  if (interactions_[i].getContactOptions().enforcement == ContactEnforcement::Penalty) {
145  // compute contribution to df_(contact)/dx (the 0, 0 block) for penalty
146  std::unique_ptr<mfem::HypreParMatrix> BTB(
147  mfem::ParMult(std::unique_ptr<mfem::HypreParMatrix>(B->Transpose()).get(), B, true));
148  delete &interaction_J->GetBlock(1, 0);
149  if (block_J->IsZeroBlock(0, 0)) {
150  mfem::Vector penalty(reference_nodes_->ParFESpace()->GetTrueVSize());
151  penalty = interactions_[i].getContactOptions().penalty;
152  BTB->ScaleRows(penalty);
153  block_J->SetBlock(0, 0, BTB.release());
154  } else {
155  block_J->SetBlock(0, 0,
156  mfem::Add(1.0, static_cast<mfem::HypreParMatrix&>(block_J->GetBlock(0, 0)),
157  interactions_[i].getContactOptions().penalty, *BTB));
158  }
159  constraint_matrices(static_cast<int>(i), 0) = nullptr;
160  } else // enforcement == ContactEnforcement::LagrangeMultiplier
161  {
162  // compute contribution to off-diagonal blocks for Lagrange multiplier
163  constraint_matrices(static_cast<int>(i), 0) = static_cast<mfem::HypreParMatrix*>(B);
164  }
165  if (interaction_J->IsZeroBlock(0, 1)) {
166  SLIC_ERROR_ROOT("Only symmetric constraint matrices are currently supported.");
167  }
168  delete &interaction_J->GetBlock(0, 1);
169  if (!interaction_J->IsZeroBlock(1, 1)) {
170  // we track our own active set, so get rid of the tribol inactive dof block
171  delete &interaction_J->GetBlock(1, 1);
172  }
173  }
174  }
175  if (haveLagrangeMultipliers()) {
176  // merge all of the contributions from all of the contact interactions
177  block_J->SetBlock(1, 0, mfem::HypreParMatrixFromBlocks(constraint_matrices));
178  // store the transpose explicitly (rather than as a TransposeOperator) for solvers that need HypreParMatrixs
179  block_J->SetBlock(0, 1, static_cast<mfem::HypreParMatrix&>(block_J->GetBlock(1, 0)).Transpose());
180  // build I_(inactive): a diagonal matrix with ones on inactive dofs and zeros elsewhere
181  mfem::Array<const mfem::Array<int>*> inactive_tdofs_vector(static_cast<int>(interactions_.size()));
182  int inactive_tdofs_ct = 0;
183  for (int i{0}; i < inactive_tdofs_vector.Size(); ++i) {
184  inactive_tdofs_vector[i] = &interactions_[static_cast<size_t>(i)].inactiveDofs();
185  inactive_tdofs_ct += inactive_tdofs_vector[i]->Size();
186  }
187  mfem::Array<int> inactive_tdofs(inactive_tdofs_ct);
188  inactive_tdofs_ct = 0;
189  for (int i{0}; i < inactive_tdofs_vector.Size(); ++i) {
190  if (inactive_tdofs_vector[i]) {
191  for (int d{0}; d < inactive_tdofs_vector[i]->Size(); ++d) {
192  inactive_tdofs[d + inactive_tdofs_ct] = (*inactive_tdofs_vector[i])[d] + pressure_dof_offsets_[i];
193  }
194  inactive_tdofs_ct += inactive_tdofs_vector[i]->Size();
195  }
196  }
197  inactive_tdofs.GetMemory().SetHostPtrOwner(false);
198  mfem::Array<int> rows(numPressureDofs() + 1);
199  rows = 0;
200  inactive_tdofs_ct = 0;
201  for (int i{0}; i < numPressureDofs(); ++i) {
202  if (inactive_tdofs_ct < inactive_tdofs.Size() && inactive_tdofs[inactive_tdofs_ct] == i) {
203  ++inactive_tdofs_ct;
204  }
205  rows[i + 1] = inactive_tdofs_ct;
206  }
207  rows.GetMemory().SetHostPtrOwner(false);
208  mfem::Vector ones(inactive_tdofs_ct);
209  ones = 1.0;
210  ones.GetMemory().SetHostPtrOwner(false);
211  mfem::SparseMatrix inactive_diag(rows.GetData(), inactive_tdofs.GetData(), ones.GetData(), numPressureDofs(),
212  numPressureDofs(), false, false, true);
213  // if the size of ones is zero, SparseMatrix creates its own memory which it
214  // owns. explicitly prevent this...
215  inactive_diag.SetDataOwner(false);
216  auto& block_1_0 = static_cast<mfem::HypreParMatrix&>(block_J->GetBlock(1, 0));
217  auto block_1_1 = new mfem::HypreParMatrix(block_1_0.GetComm(), block_1_0.GetGlobalNumRows(),
218  block_1_0.GetRowStarts(), &inactive_diag);
219  block_1_1->SetOwnerFlags(3, 3, 1);
220  block_J->SetBlock(1, 1, block_1_1);
221  // end building I_(inactive)
222  }
223  return block_J;
224 }
225 
226 void ContactData::residualFunction(const mfem::Vector& u, mfem::Vector& r)
227 {
228  const int disp_size = reference_nodes_->ParFESpace()->GetTrueVSize();
229 
230  // u_const should not change in this method; const cast is to create vector views which are copied to Tribol
231  // displacements and pressures and used to compute the (non-contact) residual
232  auto& u_const = const_cast<mfem::Vector&>(u);
233  const mfem::Vector u_blk(u_const, 0, disp_size);
234  const mfem::Vector p_blk(u_const, disp_size, numPressureDofs());
235 
236  mfem::Vector r_blk(r, 0, disp_size);
237  mfem::Vector g_blk(r, disp_size, numPressureDofs());
238 
239  double dt = 1.0;
240  setDisplacements(u_blk);
241  // we need to call update first to update gaps
242  update(1, 1.0, dt);
243  // with updated gaps, we can update pressure for contact interactions with penalty enforcement
244  setPressures(p_blk);
245  // call update again with the right pressures
246  update(1, 1.0, dt);
247 
248  r_blk += forces();
249  // calling mergedGaps() with true will zero out gap on inactive dofs (so the residual converges and the linearized
250  // system makes sense)
251  g_blk.Set(1.0, mergedGaps(true));
252 }
253 
254 std::unique_ptr<mfem::BlockOperator> ContactData::jacobianFunction(const mfem::Vector& u,
255  mfem::HypreParMatrix* orig_J) const
256 {
257  // u_const should not change in this method; const cast is to create vector views which are used to compute the
258  // (non-contact) Jacobian
259  auto& u_const = const_cast<mfem::Vector&>(u);
260  const mfem::Vector u_blk(u_const, 0, reference_nodes_->ParFESpace()->GetTrueVSize());
261 
262  auto J_contact = mergedJacobian();
263  if (J_contact->IsZeroBlock(0, 0)) {
264  J_contact->SetBlock(0, 0, orig_J);
265  } else {
266  J_contact->SetBlock(0, 0,
267  mfem::Add(1.0, *orig_J, 1.0, static_cast<mfem::HypreParMatrix&>(J_contact->GetBlock(0, 0))));
268  }
269 
270  return J_contact;
271 }
272 
273 void ContactData::setPressures(const mfem::Vector& merged_pressures) const
274 {
275  updateDofOffsets();
276  for (size_t i{0}; i < interactions_.size(); ++i) {
277  FiniteElementState p_interaction(interactions_[i].pressureSpace());
278  if (interactions_[i].getContactOptions().enforcement == ContactEnforcement::LagrangeMultiplier) {
279  // merged_pressures_const should not change; const cast is to create a vector view for copying to tribol pressures
280  auto& merged_pressures_const = const_cast<mfem::Vector&>(merged_pressures);
281  const mfem::Vector p_interaction_ref(
282  merged_pressures_const, pressure_dof_offsets_[static_cast<int>(i)],
283  pressure_dof_offsets_[static_cast<int>(i) + 1] - pressure_dof_offsets_[static_cast<int>(i)]);
284  p_interaction.Set(1.0, p_interaction_ref);
285  } else // enforcement == ContactEnforcement::Penalty
286  {
287  p_interaction.Set(interactions_[i].getContactOptions().penalty, interactions_[i].gaps());
288  }
289  for (auto dof : interactions_[i].inactiveDofs()) {
290  p_interaction[dof] = 0.0;
291  }
292  interactions_[i].setPressure(p_interaction);
293  }
294 }
295 
296 void ContactData::setDisplacements(const mfem::Vector& u)
297 {
298  reference_nodes_->ParFESpace()->GetProlongationMatrix()->Mult(u, current_coords_);
299  current_coords_ += *reference_nodes_;
300 }
301 
302 void ContactData::updateDofOffsets() const
303 {
304  if (offsets_up_to_date_) {
305  return;
306  }
307  jacobian_offsets_ = mfem::Array<int>({0, reference_nodes_->ParFESpace()->GetTrueVSize(),
308  numPressureDofs() + reference_nodes_->ParFESpace()->GetTrueVSize()});
309  pressure_dof_offsets_.SetSize(static_cast<int>(interactions_.size()) + 1);
310  pressure_dof_offsets_ = 0;
311  for (size_t i{0}; i < interactions_.size(); ++i) {
312  pressure_dof_offsets_[static_cast<int>(i + 1)] =
313  pressure_dof_offsets_[static_cast<int>(i)] + interactions_[i].numPressureDofs();
314  }
315  global_pressure_dof_offsets_.SetSize(mesh_.GetNRanks() + 1);
316  global_pressure_dof_offsets_ = 0;
317  global_pressure_dof_offsets_[mesh_.GetMyRank() + 1] = numPressureDofs();
318  MPI_Allreduce(MPI_IN_PLACE, global_pressure_dof_offsets_.GetData(), global_pressure_dof_offsets_.Size(), MPI_INT,
319  MPI_SUM, mesh_.GetComm());
320  for (int i{1}; i < mesh_.GetNRanks(); ++i) {
321  global_pressure_dof_offsets_[i + 1] += global_pressure_dof_offsets_[i];
322  }
323  if (HYPRE_AssumedPartitionCheck()) {
324  auto total_dofs = global_pressure_dof_offsets_[global_pressure_dof_offsets_.Size() - 1];
325  // If the number of ranks is less than 2, ensure the size of global_pressure_dof_offsets_ is large enough
326  if (mesh_.GetNRanks() < 2) {
327  global_pressure_dof_offsets_.SetSize(3);
328  }
329  global_pressure_dof_offsets_[0] = global_pressure_dof_offsets_[mesh_.GetMyRank()];
330  global_pressure_dof_offsets_[1] = global_pressure_dof_offsets_[mesh_.GetMyRank() + 1];
331  global_pressure_dof_offsets_[2] = total_dofs;
332  // If the number of ranks is greater than 2, shrink the size of global_pressure_dof_offsets_
333  if (mesh_.GetNRanks() > 2) {
334  global_pressure_dof_offsets_.SetSize(3);
335  }
336  }
337  offsets_up_to_date_ = true;
338 }
339 
340 #else
341 
342 ContactData::ContactData([[maybe_unused]] const mfem::ParMesh& mesh)
343  : have_lagrange_multipliers_{false}, num_pressure_dofs_{0}
344 {
345 }
346 
348 
349 void ContactData::addContactInteraction([[maybe_unused]] int interaction_id,
350  [[maybe_unused]] const std::set<int>& bdry_attr_surf1,
351  [[maybe_unused]] const std::set<int>& bdry_attr_surf2,
352  [[maybe_unused]] ContactOptions contact_opts)
353 {
354  SLIC_WARNING_ROOT("Serac built without Tribol support. No contact interaction will be added.");
355 }
356 
357 void ContactData::update([[maybe_unused]] int cycle, [[maybe_unused]] double time, [[maybe_unused]] double& dt) {}
358 
360 {
361  FiniteElementDual f(*reference_nodes_->ParFESpace(), "contact force");
362  f = 0.0;
363  return f;
364 }
365 
366 mfem::HypreParVector ContactData::mergedPressures() const { return mfem::HypreParVector(); }
367 
368 mfem::HypreParVector ContactData::mergedGaps([[maybe_unused]] bool zero_inactive) const
369 {
370  return mfem::HypreParVector();
371 }
372 
373 std::unique_ptr<mfem::BlockOperator> ContactData::mergedJacobian() const
374 {
375  jacobian_offsets_ = mfem::Array<int>(
376  {0, reference_nodes_->ParFESpace()->GetTrueVSize(), reference_nodes_->ParFESpace()->GetTrueVSize()});
377  return std::make_unique<mfem::BlockOperator>(jacobian_offsets_);
378 }
379 
380 void ContactData::residualFunction([[maybe_unused]] const mfem::Vector& u, [[maybe_unused]] mfem::Vector& r) {}
381 
382 std::unique_ptr<mfem::BlockOperator> ContactData::jacobianFunction(const mfem::Vector& u,
383  mfem::HypreParMatrix* orig_J) const
384 {
385  // u_const should not change in this method; const cast is to create vector views which are used to compute the
386  // (non-contact) Jacobian
387  auto& u_const = const_cast<mfem::Vector&>(u);
388  const mfem::Vector u_blk(u_const, 0, reference_nodes_->ParFESpace()->GetTrueVSize());
389 
390  auto J_contact = mergedJacobian();
391  if (J_contact->IsZeroBlock(0, 0)) {
392  J_contact->SetBlock(0, 0, orig_J);
393  } else {
394  J_contact->SetBlock(0, 0,
395  mfem::Add(1.0, *orig_J, 1.0, static_cast<mfem::HypreParMatrix&>(J_contact->GetBlock(0, 0))));
396  }
397 
398  return J_contact;
399 }
400 
401 void ContactData::setPressures([[maybe_unused]] const mfem::Vector& true_pressures) const {}
402 
403 void ContactData::setDisplacements([[maybe_unused]] const mfem::Vector& true_displacement) {}
404 
405 #endif
406 
407 } // namespace serac
void setDisplacements(const mfem::Vector &u)
Update the current coordinates based on the new displacement field.
void setPressures(const mfem::Vector &merged_pressures) const
Set the pressure field.
mfem::HypreParVector mergedPressures() const
Returns pressures from all contact interactions on the contact surface true degrees of freedom.
void addContactInteraction(int interaction_id, const std::set< int > &bdry_attr_surf1, const std::set< int > &bdry_attr_surf2, ContactOptions contact_opts)
Add another contact interaction.
~ContactData()
Destructor to finalize Tribol.
ContactData(const mfem::ParMesh &mesh)
The constructor.
mfem::HypreParVector mergedGaps(bool zero_inactive=false) const
Returns nodal gaps from all contact interactions on the contact surface true degrees of freedom.
void update(int cycle, double time, double &dt)
Updates the positions, forces, and Jacobian contributions associated with contact.
std::unique_ptr< mfem::BlockOperator > jacobianFunction(const mfem::Vector &u, mfem::HypreParMatrix *orig_J) const
Computes the Jacobian including contact terms, given the non-contact Jacobian terms.
FiniteElementDual forces() const
Get the contact constraint residual (i.e. nodal forces) from all contact interactions.
std::unique_ptr< mfem::BlockOperator > mergedJacobian() const
Returns a 2x2 block Jacobian on displacement/pressure true degrees of freedom from contact constraint...
void residualFunction(const mfem::Vector &u, mfem::Vector &r)
Computes the residual including contact terms.
Class for encapsulating the dual vector space of a finite element space (i.e. the space of linear for...
Class for storing contact data.
Accelerator functionality.
Definition: serac.cpp:38
Stores the options for a contact pair.