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

+6 -1
View File
@@ -113,6 +113,8 @@ pub struct StaticResult {
/// The increments of a nonlinear static step (its stresses are Cauchy stresses and its
/// strains logarithmic strains).
pub nonlinear: Option<crate::nonlinear::History>,
/// Each contact's state at the end of a nonlinear static step, by name.
pub contacts: Vec<crate::contact::ContactResult>,
}
/// One natural mode.
@@ -362,6 +364,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
|p| matches!(p, Procedure::Static {} | Procedure::CoupledTemperatureDisplacement {}),
"static or coupled temperature–displacement",
)?;
crate::system::refuse_contact(model, "linear static")?;
let mesh = Mesh::new(model, &resolved, limits.max_dofs)?;
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).chain(mesh.warnings.iter().cloned()).collect();
warnings.extend(inelastic_warnings(model));
@@ -497,7 +500,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
}
let stress: Vec<[f64; 6]> = sum.iter().zip(&count).map(|(s, c)| s.map(|v| if *c > 0 { v / f64::from(*c) } else { 0.0 })).collect();
let von_mises = stress.iter().map(von_mises).collect();
let scale = applied_total.iter().chain(&reaction_total).fold(0.0f64, |m, v| m.max(v.abs()));
let scale = crate::system::balance_scale(&applied_total, &reaction_total, &reaction);
let ground = mesh.discrete.ground_force(&u, &mesh);
let imbalance = (0..3).map(|c| (applied_total[c] + reaction_total[c] + ground[c]).abs()).fold(0.0, f64::max);
if scale > 0.0 && imbalance > 1e-6 * scale {
@@ -522,6 +525,7 @@ pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Resul
warnings,
temperature,
nonlinear: None,
contacts: Vec::new(),
})
}
@@ -568,6 +572,7 @@ pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOpt
{
return Err(Error::Invalid(format!("the shift must be a finite number, got {s}")));
}
crate::system::refuse_contact(model, "frequency")?;
let mesh = Mesh::new(model, &resolved, limits.max_dofs)?;
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).chain(mesh.warnings.iter().cloned()).collect();
warnings.extend(inelastic_warnings(model));