solver: mortar contact in nonlinear static steps

Frictionless contact pairs enforced by an augmented Lagrangian: nodal pressures from the
mortar weighted gaps, Uzawa updates after each converged increment until the gaps are within
1e-5 of the element thickness, and the consistent tangent (outer products of the weighted
gap gradients and the pressure-weighted Hessians from the automatic differentiation of the
mortar integrals). Finite sliding searches and integrates on the current configuration every
evaluation; without nonlinear geometry the pairs and their mortar integrals are fixed in the
reference configuration (small sliding, linear and exact). The tangent's pattern takes the
face pairs' blocks; the step cuts back when the active set chatters. Initial overclosure is
offset or refused; general contact, 2D models and arc length are refused by name; the linear
analyses refuse contact by name instead of the mesh.

With contact the convergence force scale comes from the converged states, the loads and the
current state. The loads-and-reactions balance check takes the nodal reactions into its scale
(no spurious warning for a part squeezed between held faces).

Tests: the contact patch test on non-matching hex8/tet4 meshes (exact with small sliding,
to 1e-4 with finite sliding), Hertz cylinder on a flat (peak pressure within 3%, half-width
bracketed) with quadratic Newton convergence, overclosure, thread-count determinism and the
refusals; Hertz sphere on a flat (heavy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 14:58:32 -04:00
1 parent 37f338c9e8
commit 8871f6721d
8 files changed
+1392 -25

No files matched your search

+18 -5
View File
@@ -426,11 +426,6 @@ impl Mesh {
/// Build it from the model and its resolved view, refusing what the solver cannot represent.
pub fn new(model: &Model, resolved: &Resolved, max_dofs: usize) -> Result<Mesh> {
if let Some(name) = model.contacts().keys().next() {
return Err(Error::NotSupported(format!(
"contact (`{name}`) in implicit analyses (it comes with M7): a model's contacts apply to all its steps, so solve its explicit dynamic steps, or remove the contacts to solve the implicit ones"
)));
}
let mut elements = Vec::new();
let mut trusses = Vec::new();
let mut structural = Vec::new();
@@ -661,6 +656,24 @@ impl Mesh {
}
}
/// The scale of the loads-and-reactions balance check: the largest load or reaction, nodal ones
/// too, since reactions that cancel out (a part squeezed between held faces) still set it.
pub(crate) fn balance_scale(applied: &[f64; 3], total: &[f64; 3], reaction: &[[f64; 3]]) -> f64 {
let largest = reaction.iter().flatten().fold(0.0f64, |m, v| m.max(v.abs()));
applied.iter().chain(total).fold(largest, |m, v| m.max(v.abs()))
}
/// Refuse contact in an analysis that cannot take it (linear static, frequency, buckling): a
/// model's contacts apply to all its steps.
pub fn refuse_contact(model: &Model, analysis: &str) -> Result<()> {
match model.contacts().keys().next() {
Some(name) => Err(Error::NotSupported(format!(
"contact (`{name}`) in {analysis} steps: a model's contacts apply to all its steps, so solve its nonlinear static or explicit dynamic steps, or remove the contacts to solve this one"
))),
None => Ok(()),
}
}
/// The prescribed value of every degree of freedom (`None` = free) under a step's boundary
/// conditions; `values: false` holds them at zero (frequency steps). Rotations of solid nodes do
/// not exist and are ignored, like the directions of a reference point no coupling ties