solver: mortar contact in 2D models

Plane strain, plane stress and axisymmetric models take contact pairs and
general (self) contact on the linear edges of tri3 and quad4 elements:
edges oriented out of their elements, the edge mortar of fem with the
thickness or 2 pi r in the weights, the element's area over the edge's
length as the thickness under it, and the in-plane rows of the tangent
blocks. Verified by the contact patch test in plane strain and
axisymmetric (to 1e-9 small sliding, 1e-4 finite), Hertz's line contact
on quad4 edges (Johnson 1985, 4.3; equal to the hex8 model's to 1e-5),
and a hybrid rubber C-ring closing on itself that moves node for node as
the one-layer solid ring.

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 16:30:44 -04:00
1 parent 6baa189231
commit 227c4544ed
20 files changed
+1579 -81

No files matched your search

+2 -1
View File
@@ -1,7 +1,8 @@
//! `interaction.contact.*`: contact definitions (pairs of surfaces, general contact) and the
//! search for surfaces that touch; `interaction.tie*`: ties between non-matching meshes.
//!
//! A contact's surfaces are sets of element faces ([`fieldcraft_model::contact`]). The commands
//! A contact's surfaces are sets of element faces, or of element edges in a 2D model
//! ([`fieldcraft_model::contact`]). The commands
//! take the friction, penalty scale and initial-penetration choice of the model's form; `edit`
//! replaces a definition with the model's JSON form, `delete` removes it.
//!
+6 -3
View File
@@ -388,13 +388,16 @@ impl Model {
}
}
// Contacts: their surfaces are element faces of solids.
// Contacts: their surfaces are element faces of solids, or edges of 2D elements.
for (name, c) in &self.contacts {
for set in c.surfaces.sets() {
let faces = &set_of(set).faces;
let solid = |f: &crate::ElementFace| self.elements.get(&f.element).is_some_and(|e| e.kind.dimension() == 3);
if faces.is_empty() {
d.error(format!("contact `{name}`"), format!("set `{set}` has no element faces (a contact surface is a set of faces)"));
if faces.is_empty() && resolved.planar_edges(set).is_empty() {
d.error(
format!("contact `{name}`"),
format!("set `{set}` has no element faces (a contact surface is a set of faces, or of 2D element edges)"),
);
} else if !faces.iter().all(solid) {
d.error(
format!("contact `{name}`"),
+2 -2
View File
@@ -3,12 +3,12 @@
//! A contact is a model definition, active in every step that can take it (explicit dynamics,
//! and nonlinear static steps; the linear analyses refuse contact by name). Its
//! surfaces are sets with element
//! faces, so they survive remeshing like a pressure load does:
//! faces (in a 2D model, element edges), so they survive remeshing like a pressure load does:
//! - a **pair**: the faces of `main` against those of `secondary`, both ways (each surface's
//! nodes are kept out of the other's faces, so neither surface is privileged);
//! - **general** contact: every face against every other face of one surface (single-surface or
//! self contact, for folding and buckling), or of every exterior face of the model's solid
//! elements when no surface is named.
//! elements (in a 2D model every exterior edge of its 2D elements) when no surface is named.
//!
//! **Friction** is Coulomb's law with a coefficient that falls from its static to its kinetic
//! value with the slip rate γ̇: μ = μ_k + (μ_s − μ_k) e^{−d γ̇} (the exponential decay of
+147 -61
View File
@@ -81,17 +81,25 @@
//! and that it does not reach through more than half an element (the far side of a thin wall),
//! so two faces pressing together are paired both ways, symmetrically: self-contact of folds.
//!
//! **2D models.** In plane strain, plane stress and axisymmetric models the surfaces are element
//! edges (sets' edges; general contact defaults to every exterior edge of the 2D elements),
//! integrated by [`fieldcraft_fem::mortar::integrate_edges`] with the out-of-plane measure
//! (thickness, or 2π r) in the weights, so A_A, g̃_A and the forces are those of the whole ring
//! or slab. Each edge is oriented so that its normal points out of its element; the element's
//! thickness under it is its area over the edge's length. Points keep z = 0 and the 2D nodes have
//! no Z translation, so the tangent blocks keep their in-plane rows.
//!
//! **Scope.** Linear solid faces (tri3 and quad4 faces: tet4, wedge6 and hex8 elements) in 3D
//! models, with Coulomb friction ([`Interface::friction`]). Quadratic faces, 2D models and
//! arc-length steps are refused by name.
//! models and linear edges (tri3 and quad4 elements) in 2D ones, with Coulomb friction
//! ([`Interface::friction`]). Quadratic faces and edges and arc-length steps are refused by name.
use std::collections::BTreeMap;
use fieldcraft_fem::contact::{Aabb, Grid, bounds};
use fieldcraft_fem::dual::{Dual, Hyper, Real, VARS};
use fieldcraft_fem::frame::{Vec3, dot, norm, unit};
use fieldcraft_fem::mortar::{area_vector, integrate};
use fieldcraft_model::{Contact, ContactSurfaces, ElementFace, ElementKind, Friction, InitialPenetration, Model, Resolved};
use fieldcraft_fem::mortar::{Sample, area_vector, edge_area_vector, integrate, integrate_edges};
use fieldcraft_model::{Contact, ContactSurfaces, ElementEdge, ElementFace, ElementKind, Extent, Friction, InitialPenetration, Model, Resolved};
use fieldcraft_parallel as parallel;
use serde::Serialize;
@@ -136,7 +144,8 @@ const GRAIN: usize = 16;
#[derive(Debug, Clone)]
pub(crate) struct Face {
pub(crate) nodes: Vec<usize>,
pub(crate) dofs: Vec<[usize; 3]>,
/// X, Y and Z translations per corner (a 2D model's nodes have no Z).
pub(crate) dofs: Vec<[Option<usize>; 3]>,
/// The nodes of its element (never in contact with the face).
owner: Vec<usize>,
/// The penetration beyond which a point has passed through, and its element's thickness.
@@ -166,6 +175,8 @@ pub struct Interface {
fixed: Option<Vec<Vec<Fixed>>>,
/// Coulomb friction (frictionless: none).
friction: Option<Friction>,
/// A 2D model's out-of-plane measure (its surfaces are edges); `None`: solid faces.
extent: Option<Extent>,
}
/// A contact's state carried from one converged increment to the next: per secondary node the
@@ -277,7 +288,7 @@ pub struct ContactResult {
struct PairTerms {
/// Mesh slots of the pair's corners (secondary first) and their degrees of freedom.
nodes: Vec<usize>,
dofs: Vec<[usize; 3]>,
dofs: Vec<[Option<usize>; 3]>,
/// Secondary node indices (in [`Interface::nodes`]) of the secondary corners.
corners: Vec<usize>,
/// ∂g̃_a/∂x per secondary corner (24 variables; corner c of the pair at 3c, main corners from 12).
@@ -290,20 +301,42 @@ struct PairTerms {
/// translations (row-major, node by node).
pub type Blocks = Vec<(Vec<usize>, Vec<f64>)>;
fn position(mesh: &Mesh, u: &[f64], node: usize, dofs: &[usize; 3]) -> Vec3 {
fn position(mesh: &Mesh, u: &[f64], node: usize, dofs: &[Option<usize>; 3]) -> Vec3 {
let x = mesh.xyz.get(node).copied().unwrap_or_default();
[0, 1, 2].map(|k| x[k] + u.get(dofs[k]).copied().unwrap_or(0.0))
[0, 1, 2].map(|k| x[k] + dofs[k].and_then(|d| u.get(d)).copied().unwrap_or(0.0))
}
/// The element's volume (hex8, tet4) for the thickness under a face.
/// The element's volume (hex8, tet4), or a 2D element's area (tri3, quad4), for the thickness
/// under a face or edge.
fn volume(kind: ElementKind, x: &[Vec3]) -> Option<f64> {
match kind {
ElementKind::Hex8 => <[Vec3; 8]>::try_from(x).ok().and_then(|a| fieldcraft_fem::uniform::Hex8::hex8(&a).ok()).map(|k| k.volume),
ElementKind::Tet4 => <[Vec3; 4]>::try_from(x).ok().and_then(|a| fieldcraft_fem::uniform::Tet4::tet4(&a).ok()).map(|k| k.volume),
ElementKind::Tri3 | ElementKind::Quad4 => {
// The shoelace formula over the corners in the XY plane.
let doubled: f64 = x.iter().zip(x.iter().cycle().skip(1)).map(|(a, b)| a[0] * b[1] - a[1] * b[0]).sum();
Some(0.5 * doubled.abs())
}
_ => None,
}
}
/// The overlap integration of a face pair (`extent`: `None`) or an edge pair of a 2D model.
fn overlap<T: Real>(extent: Option<Extent>, xs: &[[T; 3]], xm: &[[T; 3]], f: impl FnMut(&Sample<T>)) -> Option<()> {
match extent {
None => integrate(xs, xm, f),
Some(e) => integrate_edges(xs, xm, e, f),
}
}
/// A face's area vector, or an edge's ([`edge_area_vector`]) in a 2D model.
fn area_of(extent: Option<Extent>, x: &[Vec3]) -> Option<Vec3> {
match extent {
None => area_vector(x),
Some(e) => edge_area_vector(x, e),
}
}
fn size(x: &[Vec3]) -> f64 {
let b = bounds(x, 0.0);
(0..3).map(|k| b[1][k] - b[0][k]).fold(0.0, f64::max)
@@ -315,37 +348,86 @@ pub(crate) fn faces(model: &Model, resolved: &Resolved, mesh: &Mesh, owner: &str
face_list(model, mesh, owner, resolved.sets.get(set).map(|s| s.faces.as_slice()).unwrap_or_default())
}
/// The edges of set `set` of a 2D model for definition `owner`, refused unless they are linear
/// edges of analysed 2D elements.
fn edges(model: &Model, resolved: &Resolved, mesh: &Mesh, owner: &str, set: &str) -> Result<Vec<Face>> {
edge_list(model, mesh, owner, resolved.sets.get(set).map(|s| s.edges.as_slice()).unwrap_or_default())
}
/// The faces `list` for definition `owner`, as [`faces`].
fn face_list(model: &Model, mesh: &Mesh, owner: &str, list: &[ElementFace]) -> Result<Vec<Face>> {
let mut out = Vec::with_capacity(list.len());
for f in list {
let at = |what: String| Error::Invalid(format!("{owner}: element {} face {}: {what}", f.element, f.face));
let el = mesh.element(f.element).ok_or_else(|| at("the element is not a solid element of the analysis".into()))?;
let labels = model.face_nodes(*f).ok_or_else(|| at("no such face".into()))?;
if !matches!(labels.len(), 3 | 4) {
return Err(Error::NotSupported(format!(
"{owner} on the faces of {} elements (element {}) in implicit analyses; it takes the linear faces of tet4, wedge6 and hex8 elements",
el.kind.name(),
f.element
)));
}
let nodes: Vec<usize> = labels.iter().map(|l| mesh.node(*l).ok_or_else(|| at(format!("node {l} is not analysed")))).collect::<Result<_>>()?;
let dofs = nodes
.iter()
.map(|n| translations(mesh, *n).and_then(|d| <[usize; 3]>::try_from(d).map_err(|_| Error::Internal("solid node translations".into()))))
.collect::<Result<Vec<_>>>()?;
let x: Vec<Vec3> = nodes.iter().map(|n| mesh.xyz.get(*n).copied().unwrap_or_default()).collect();
let area = area_vector(&x).map_or(0.0, norm);
let thickness = volume(el.kind, &el.xyz).filter(|v| *v > 0.0 && area > 0.0).map_or(size(&x), |v| v / area);
if !(thickness > 0.0 && thickness.is_finite()) {
return Err(at("the face has no size".into()));
}
let shear = (el.d[3][3] + el.d[4][4] + el.d[5][5]) / 3.0;
let mut owner = el.nodes.clone();
owner.sort_unstable();
out.push(Face { nodes, dofs, owner, depth: DEPTH_FRACTION * thickness, thickness, stiffness: PENALTY_FACTOR * 3.0 * shear / thickness });
list.iter()
.map(|f| {
let at = |what: String| Error::Invalid(format!("{owner}: element {} face {}: {what}", f.element, f.face));
let labels = model.face_nodes(*f).ok_or_else(|| at("no such face".into()))?;
surface_face(mesh, owner, f.element, labels, false, at)
})
.collect()
}
/// The edges `list` of a 2D model for definition `owner`, as [`edges`].
fn edge_list(model: &Model, mesh: &Mesh, owner: &str, list: &[ElementEdge]) -> Result<Vec<Face>> {
list.iter()
.map(|e| {
let at = |what: String| Error::Invalid(format!("{owner}: element {} edge {}: {what}", e.element, e.edge));
let labels = model.edge_nodes(*e).ok_or_else(|| at("no such edge".into()))?;
surface_face(mesh, owner, e.element, labels, true, at)
})
.collect()
}
/// One face (or, `edge`, one edge of a 2D element) of element `element` with nodes `labels`.
fn surface_face(mesh: &Mesh, owner: &str, element: u32, labels: Vec<u32>, edge: bool, at: impl Fn(String) -> Error) -> Result<Face> {
let el = mesh.element(element).ok_or_else(|| at("the element is not a solid or 2D element of the analysis".into()))?;
let (linear, takes) = if edge {
(labels.len() == 2 && el.kind.dimension() == 2, "the linear edges of tri3 and quad4 elements")
} else {
(matches!(labels.len(), 3 | 4) && el.kind.dimension() == 3, "the linear faces of tet4, wedge6 and hex8 elements")
};
if !linear {
let what = if edge { "edges" } else { "faces" };
return Err(Error::NotSupported(format!(
"{owner} on the {what} of {} elements (element {element}) in implicit analyses; it takes {takes}",
el.kind.name()
)));
}
Ok(out)
let mut nodes: Vec<usize> = labels.iter().map(|l| mesh.node(*l).ok_or_else(|| at(format!("node {l} is not analysed")))).collect::<Result<_>>()?;
let mut x: Vec<Vec3> = nodes.iter().map(|n| mesh.xyz.get(*n).copied().unwrap_or_default()).collect();
if edge {
// Outward: the edge's normal (t_y, −t_x) away from its element's centroid.
let count = el.xyz.len().max(1) as f64;
let centroid: Vec3 = std::array::from_fn(|k| el.xyz.iter().map(|p| p[k]).sum::<f64>() / count);
let (Some(a), Some(b)) = (x.first(), x.get(1)) else { return Err(at("the edge has no size".into())) };
let mid = [0.5 * (a[0] + b[0]) - centroid[0], 0.5 * (a[1] + b[1]) - centroid[1]];
if (b[1] - a[1]) * mid[0] - (b[0] - a[0]) * mid[1] < 0.0 {
nodes.reverse();
x.reverse();
}
}
let dofs = nodes
.iter()
.map(|n| {
let d = translations(mesh, *n)?;
match d.as_slice() {
[a, b, c] => Ok([Some(*a), Some(*b), Some(*c)]),
[a, b] if edge => Ok([Some(*a), Some(*b), None]),
_ => Err(Error::Internal("node translations".into())),
}
})
.collect::<Result<Vec<_>>>()?;
// The face's area, or the edge's length.
let area = match x.as_slice() {
[a, b] if edge => norm([b[0] - a[0], b[1] - a[1], 0.0]),
_ => area_vector(&x).map_or(0.0, norm),
};
let thickness = volume(el.kind, &el.xyz).filter(|v| *v > 0.0 && area > 0.0).map_or(size(&x), |v| v / area);
if !(thickness > 0.0 && thickness.is_finite()) {
return Err(at(format!("the {} has no size", if edge { "edge" } else { "face" })));
}
let shear = (el.d[3][3] + el.d[4][4] + el.d[5][5]) / 3.0;
let mut owner = el.nodes.clone();
owner.sort_unstable();
Ok(Face { nodes, dofs, owner, depth: DEPTH_FRACTION * thickness, thickness, stiffness: PENALTY_FACTOR * 3.0 * shear / thickness })
}
/// A face pair of small-sliding contact: its mortar integrals in the reference configuration,
@@ -401,9 +483,9 @@ struct Found {
}
/// The weighted gaps g̃_a of one face pair (corners `xs`, `xm`) over any scalar.
fn weighted_gaps<T: Real>(xs: &[[T; 3]], xm: &[[T; 3]]) -> Option<[T; 4]> {
fn weighted_gaps<T: Real>(extent: Option<Extent>, xs: &[[T; 3]], xm: &[[T; 3]]) -> Option<[T; 4]> {
let mut g = [T::cst(0.0); 4];
integrate(xs, xm, |p| {
overlap(extent, xs, xm, |p| {
let w = p.weight * p.gap();
for a in 0..4 {
g[a] = g[a] + w * p.secondary[a];
@@ -418,9 +500,9 @@ fn weighted_gaps<T: Real>(xs: &[[T; 3]], xm: &[[T; 3]]) -> Option<[T; 4]> {
/// corners from 0, main corners from 12), with the integrals on the current configuration: the
/// rate form of the mortar slip, B_a = [∫ Φ_a N_i P, −∫ Φ_a N_j P] (McDevitt & Laursen 2000; Puso &
/// Laursen 2004), P = I − n n.
fn slip_operator(xs: &[Vec3], xm: &[Vec3]) -> Option<[[[f64; VARS]; 3]; 4]> {
fn slip_operator(extent: Option<Extent>, xs: &[Vec3], xm: &[Vec3]) -> Option<[[[f64; VARS]; 3]; 4]> {
let mut b = [[[0.0; VARS]; 3]; 4];
integrate(xs, xm, |p| {
overlap(extent, xs, xm, |p| {
let n = p.normal;
for a in 0..4 {
let wa = p.weight * p.secondary[a];
@@ -470,17 +552,20 @@ impl Interface {
fn new(model: &Model, resolved: &Resolved, mesh: &Mesh, name: &str, contact: &Contact) -> Result<Interface> {
let owner = format!("contact `{name}`");
let extent = mesh.theory.map(|t| t.extent());
// A set's faces or (2D) edges, or every exterior one of the model.
let surface = |set: Option<&String>| match (set, extent) {
(Some(s), None) => faces(model, resolved, mesh, &owner, s),
(Some(s), Some(_)) => edges(model, resolved, mesh, &owner, s),
(None, None) => face_list(model, mesh, &owner, &model.exterior_faces()),
(None, Some(_)) => edge_list(model, mesh, &owner, &model.exterior_edges()),
};
let (secondary, main) = match &contact.surfaces {
ContactSurfaces::Pair { main, secondary } => {
(faces(model, resolved, mesh, &owner, secondary)?, faces(model, resolved, mesh, &owner, main)?)
}
ContactSurfaces::Pair { main, secondary } => (surface(Some(secondary))?, surface(Some(main))?),
// Single surface: every face against every other one that faces it and shares no node
// with its element (its neighbours), both ways.
ContactSurfaces::General { surface } => {
let list = match surface {
Some(set) => faces(model, resolved, mesh, &owner, set)?,
None => face_list(model, mesh, &owner, &model.exterior_faces())?,
};
ContactSurfaces::General { surface: set } => {
let list = surface(set.as_ref())?;
(list.clone(), list)
}
};
@@ -493,7 +578,7 @@ impl Interface {
// ∫ Φ_a over the face itself (the face against its own mirror image).
let mirror: Vec<Vec3> = x.iter().rev().copied().collect();
let mut own = [0.0; 4];
integrate(&x, &mirror, |p| {
overlap(extent, &x, &mirror, |p| {
for a in 0..4 {
own[a] += p.weight * p.secondary[a];
}
@@ -528,6 +613,7 @@ impl Interface {
offset,
fixed: None,
friction: (!contact.friction.is_frictionless()).then_some(contact.friction),
extent,
})
}
@@ -609,7 +695,7 @@ impl Interface {
.filter_map(|f| {
let xm = xm.get(f.main)?;
let (mut d, mut m, mut normal) = ([[0.0; 4]; 4], [[0.0; 4]; 4], [0.0; 3]);
integrate(xs, xm, |p| {
overlap(self.extent, xs, xm, |p| {
normal = p.normal;
for a in 0..4 {
for i in 0..4 {
@@ -631,12 +717,12 @@ impl Interface {
let main_x: Vec<Vec<Vec3>> = self.main.iter().map(|f| Interface::corners(mesh, u, f)).collect();
let boxes: Vec<Aabb> = self.main.iter().zip(&main_x).map(|(f, x)| bounds(x, reach * f.depth)).collect();
let grid = Grid::new(&boxes, 4 * boxes.len() + 64);
let main_normals: Vec<Option<Vec3>> = main_x.iter().map(|x| area_vector(x).filter(|a| norm(*a) > 0.0)).collect();
let main_normals: Vec<Option<Vec3>> = main_x.iter().map(|x| area_of(self.extent, x).filter(|a| norm(*a) > 0.0)).collect();
Ok(parallel::map_grained(self.secondary.len(), GRAIN, |i| {
let mut found = Vec::new();
let Some(f) = self.secondary.get(i) else { return keep(&[], &main_x, found) };
let xs = Interface::corners(mesh, u, f);
let Some(ns) = area_vector(&xs).filter(|a| norm(*a) > 0.0) else { return keep(&xs, &main_x, found) };
let Some(ns) = area_of(self.extent, &xs).filter(|a| norm(*a) > 0.0) else { return keep(&xs, &main_x, found) };
let mut candidates = Vec::new();
grid.candidates_in(&bounds(&xs, 0.0), &mut candidates);
for &c in &candidates {
@@ -649,7 +735,7 @@ impl Interface {
continue;
}
let (mut gap, mut support, mut deepest, mut any) = ([0.0; 4], [0.0; 4], f64::INFINITY, false);
integrate(&xs, xm, |p| {
overlap(self.extent, &xs, xm, |p| {
any = true;
let g = p.gap();
deepest = deepest.min(g);
@@ -741,7 +827,7 @@ impl Interface {
slipping: Vec::new(),
};
for (f, corners) in self.secondary.iter().zip(&corner_index) {
let Some(a) = area_vector(&Interface::corners(mesh, u, f)) else { continue };
let Some(a) = area_of(self.extent, &Interface::corners(mesh, u, f)) else { continue };
for i in corners {
if let Some(v) = state.normal.get_mut(*i) {
(0..3).for_each(|k| v[k] += a[k]);
@@ -786,7 +872,7 @@ impl Interface {
((0..corners.len()).map(|a| fixed.gradient(a)).collect(), None)
} else if tangent {
let (vs, vm) = variables(&xs, &xm, Hyper::var);
let Some(g) = weighted_gaps(&vs, &vm) else { return Ok(None) };
let Some(g) = weighted_gaps(self.extent, &vs, &vm) else { return Ok(None) };
let mut h = vec![0.0; fieldcraft_fem::dual::PACKED];
for (ga, p) in g.iter().zip(&pressure) {
h.iter_mut().zip(&ga.h).for_each(|(x, y)| *x += p * y);
@@ -794,7 +880,7 @@ impl Interface {
(g.iter().take(corners.len()).map(|ga| ga.g).collect::<Vec<_>>(), Some(h))
} else {
let (vs, vm) = variables(&xs, &xm, Dual::var);
let Some(g) = weighted_gaps(&vs, &vm) else { return Ok(None) };
let Some(g) = weighted_gaps(self.extent, &vs, &vm) else { return Ok(None) };
(g.iter().take(corners.len()).map(|ga| ga.g).collect(), None)
};
let nodes = f.nodes.iter().chain(&m.nodes).copied().collect();
@@ -815,7 +901,7 @@ impl Interface {
let mut on = [0.0; 3];
for k in 0..3 {
let f = -p * gv[k];
if let Some(x) = internal.get_mut(dofs[k]) {
if let Some(x) = dofs[k].and_then(|d| internal.get_mut(d)) {
*x += f;
}
on[k] = -f;
@@ -917,7 +1003,7 @@ impl Interface {
let (rs, rm) = (Interface::corners(mesh, reference, f), Interface::corners(mesh, reference, m));
let operator: Vec<[[f64; VARS]; 3]> = match self.fixed.as_ref().and_then(|f| f.get(s)).and_then(|l| l.get(k)) {
Some(fixed) => (0..corners.len()).map(|a| fixed.slip_jacobian(a)).collect(),
None => slip_operator(&xs, &xm)?.into_iter().take(corners.len()).collect(),
None => slip_operator(self.extent, &xs, &xm)?.into_iter().take(corners.len()).collect(),
};
let mut delta = [0.0; VARS];
for (i, (x, r)) in xs.iter().zip(&rs).enumerate() {
@@ -929,7 +1015,7 @@ impl Interface {
let slips: Vec<(Vec3, [[f64; VARS]; 3])> =
operator.into_iter().map(|b| (b.map(|row| row.iter().zip(&delta).map(|(x, y)| x * y).sum()), b)).collect();
let nodes: Vec<usize> = f.nodes.iter().chain(&m.nodes).copied().collect();
let dofs: Vec<[usize; 3]> = f.dofs.iter().chain(&m.dofs).copied().collect();
let dofs: Vec<[Option<usize>; 3]> = f.dofs.iter().chain(&m.dofs).copied().collect();
Some((nodes, dofs, corners.clone(), slips))
});
let terms: Vec<_> = terms.into_iter().flatten().collect();
@@ -997,7 +1083,7 @@ impl Interface {
for (c, d) in dofs.iter().enumerate() {
let on: Vec3 = std::array::from_fn(|l| (0..3).map(|k| t[k] * jac[k].get(var(corners.len(), c, l)).copied().unwrap_or(0.0)).sum());
for l in 0..3 {
if let Some(x) = internal.get_mut(d[l]) {
if let Some(x) = d[l].and_then(|d| internal.get_mut(d)) {
*x -= on[l];
}
}
+8 -9
View File
@@ -123,7 +123,7 @@ use crate::constraints::Reduction;
use crate::contact::{self, ContactResult, Interface};
use crate::ldlt::{Kind, Symmetric};
use crate::sparse::{Csr, NodeGraph, add_block, add_entry};
use crate::system::{Mesh, add_element_thermal_loads, loads, temperature_loads, translations};
use crate::system::{Mesh, add_element_thermal_loads, loads, temperature_loads, translation_block, translations};
use crate::{Error, Limits, Result};
/// One Newton iteration of an increment.
@@ -445,8 +445,10 @@ impl Problem<'_> {
}
});
if let Some(k) = k.as_mut() {
// Contact blocks are over X, Y and Z per node; a 2D model keeps the in-plane rows.
let kinds = self.mesh.translation_kinds();
for (nodes, data) in &blocks {
add_block(k, layout, nodes, self.mesh.translation_kinds(), data)?;
add_block(k, layout, nodes, kinds, &translation_block(data, nodes.len(), kinds))?;
}
}
let mut history = Vec::with_capacity(self.mesh.elements.len());
@@ -945,13 +947,10 @@ fn check_supported(model: &Model, mesh: &Mesh, step: &Step, settings: &Nonlinear
return Err(at(format!("centrifugal loads (load `{name}`), which depend on the deformed position")));
}
plasticity(model, step, settings, warnings)?;
if let Some(name) = model.contacts().keys().next() {
if mesh.theory.is_some() {
return Err(at(format!("contact (`{name}`) in 2D models")));
}
if settings.arc_length.is_some() {
return Err(at(format!("contact (`{name}`) with arc-length control")));
}
if let Some(name) = model.contacts().keys().next()
&& settings.arc_length.is_some()
{
return Err(at(format!("contact (`{name}`) with arc-length control")));
}
if settings.nonlinear_geometry
&& let Some(name) = model.couplings().keys().next()
+106
View File
@@ -700,3 +700,109 @@ fn a_rubber_bellows_folds_onto_itself() {
assert!(c.closed() > 0, "the folds touch");
assert!(c.penetration < 1e-3 * wall, "{}", c.penetration);
}
/// The C-ring of [`c_ring`] as a plane strain model (quad4, hybrid, 0.2 thick: the hex8 layer's
/// thickness) with general contact on every exterior edge.
fn planar_c_ring(closing: f64, threads: usize) -> (Blocks, StaticResult) {
let mut b = Blocks::default();
let (n, open) = (36usize, 5f64.to_radians());
let angle = move |i: f64| open + i * (2.0 * std::f64::consts::PI - 2.0 * open);
let label = |i: usize, j: usize| 1 + (i + (n + 1) * j) as u32;
for j in 0..=2 {
for i in 0..=n {
let (t, r) = (angle(i as f64 / n as f64), 1.5 - 0.25 * j as f64);
let x = [r * t.cos(), r * t.sin(), 0.0];
b.model.set_node(label(i, j), x).unwrap();
b.xyz.insert(label(i, j), x);
}
}
let mut next = 0;
for j in 0..2 {
for i in 0..n {
next += 1;
// Counter-clockwise in the plane: along the outer arc, then inwards and back.
b.model.set_element(next, ElementKind::Quad4, vec![label(i, j), label(i + 1, j), label(i + 1, j + 1), label(i, j + 1)]).unwrap();
}
}
let all: Vec<u32> = b.model.elements().keys().copied().collect();
b.model.add_set("all", Set { elements: all, ..Set::default() }).unwrap();
let rubber = Material {
elastic: Some(Elastic::Isotropic { young: 3.0, poisson: 0.499 }),
hyperelastic: Some(Hyperelastic::MooneyRivlin { c01: 0.1 }),
..Material::default()
};
b.model.add_material("Rubber", rubber).unwrap();
let section = Section {
set: "all".into(),
material: "Rubber".into(),
kind: SectionKind::PlaneStrain { thickness: 0.2 },
formulation: Formulation { hybrid: true, ..Formulation::default() },
};
b.model.add_section("ring", section).unwrap();
let settings = NonlinearStatic { initial_increment: 0.1, max_increments: 200, ..NonlinearStatic::default() };
b.model.add_step(Step::new("Step", Procedure::NonlinearStatic(settings)), None).unwrap();
b.model.add_contact("self", Contact::new(ContactSurfaces::General { surface: None })).unwrap();
let step = 2.0 * std::f64::consts::PI - 2.0 * open;
let at = |x: [f64; 3], i: f64| {
((x[0] * x[0] + x[1] * x[1]).sqrt() - 1.5).abs() < 1e-9
&& (x[1].atan2(x[0]).rem_euclid(2.0 * std::f64::consts::PI) - angle(i)).abs() < 1e-6 * step
};
let near_end = 2.0 / n as f64;
b.bc("upper", [None, Some(-closing), None], move |x| at(x, near_end));
b.bc("lower", [None, Some(closing), None], move |x| at(x, 1.0 - near_end));
b.bc("back", [Some(0.0), Some(0.0), None], move |x| at(x, 0.5));
let r = fieldcraft_parallel::with_threads(threads, || solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap());
(b, r)
}
/// 2D self-contact: the plane strain ring closes on itself like the one-layer solid ring with
/// its z held (the same problem), the edges' mortar pressing as the faces' does; bit for bit on
/// any thread count.
#[test]
fn a_plane_strain_ring_closes_on_itself_as_the_solid_ring_does() {
let (b2, r2) = planar_c_ring(0.15, 1);
let (b3, r3) = c_ring(true, 0.15);
let (c2, c3) = (&r2.contacts[0], &r3.contacts[0]);
assert!(c2.closed() > 0 && c2.penetration < 1e-4, "{} {}", c2.closed(), c2.penetration);
// Each edge node closes where its face nodes do (two per edge node, at z = 0 and 0.2).
assert_eq!(2 * c2.closed(), c3.closed());
// The same motion: every node of the plane against the solid's node at z = 0.
let mut checked = 0;
for (l, x) in &b2.xyz {
let Some((m, _)) = b3.xyz.iter().find(|(_, y)| (y[0] - x[0]).abs() < 1e-9 && (y[1] - x[1]).abs() < 1e-9 && y[2] == 0.0) else { continue };
let (u, v) = (r2.displacement[r2.nodes.binary_search(l).unwrap()], r3.displacement[r3.nodes.binary_search(m).unwrap()]);
assert!((u[0] - v[0]).abs() < 1e-5 && (u[1] - v[1]).abs() < 1e-5, "node {l}: {u:?} {v:?}");
checked += 1;
}
assert_eq!(checked, b2.xyz.len());
let (_, many) = planar_c_ring(0.15, 4);
assert_eq!(r2.displacement, many.displacement);
assert_eq!(r2.contacts, many.contacts);
}
#[test]
fn what_2d_contact_refuses() {
// Quadratic edges.
let (mut b, _) = planar_c_ring(0.0, 1);
let mut m = Model::default();
for (i, x) in [[0., 0.], [1., 0.], [1., 1.], [0., 1.], [0.5, 0.], [1., 0.5], [0.5, 1.], [0., 0.5]].iter().enumerate() {
m.set_node(i as u32 + 1, [x[0], x[1], 0.0]).unwrap();
}
m.set_element(1, ElementKind::Quad8, (1..=8).collect()).unwrap();
m.add_set("all", Set { elements: vec![1], ..Set::default() }).unwrap();
m.add_material("Mat", Material { elastic: Some(Elastic::Isotropic { young: 1.0, poisson: 0.3 }), ..Material::default() }).unwrap();
m.add_section(
"s",
Section { set: "all".into(), material: "Mat".into(), kind: SectionKind::PlaneStrain { thickness: 1.0 }, formulation: Formulation::default() },
)
.unwrap();
m.add_step(Step::new("Step", Procedure::NonlinearStatic(NonlinearStatic::default())), None).unwrap();
m.add_contact("self", Contact::new(ContactSurfaces::General { surface: None })).unwrap();
let e = solve_nonlinear(&m, Some("Step"), &Limits::default()).unwrap_err().to_string();
assert!(e.contains("edges of quad8") && e.contains("tri3 and quad4"), "{e}");
// A plane strain ring with arc-length control.
let arc = NonlinearStatic { arc_length: Some(ArcLength::default()), ..NonlinearStatic::default() };
b.model.replace_step(Step { procedure: Procedure::NonlinearStatic(arc), ..b.model.step("Step").unwrap().clone() }).unwrap();
let e = solve_nonlinear(&b.model, Some("Step"), &Limits::default()).unwrap_err().to_string();
assert!(e.contains("arc-length"), "{e}");
}
+21 -4
View File
@@ -468,8 +468,20 @@ contact patch test. General (single-surface) contact uses one face list, a set's
exterior face of the solids, as both sides: each face against every other one facing it that
shares no node with its element (its neighbours) and that it does not reach through more than
half an element (the far side of a thin wall), paired both ways, so the folds of one body press
on each other symmetrically. Linear faces (tri3, quad4: tet4, wedge6, hex8); quadratic faces, 2D
models and arc-length steps are refused by name.
on each other symmetrically. Linear faces (tri3, quad4: tet4, wedge6, hex8), and in 2D models
(plane strain, plane stress, axisymmetric) the linear edges of tri3 and quad4 elements (sets'
edges; general contact defaults to every exterior edge); quadratic faces and edges and
arc-length steps are refused by name.
**2D models**: each edge is oriented with its normal (t_y, −t_x) out of its element; the main
edge is projected along the secondary edge's normal onto its line, which maps main points to the
secondary parameter linearly, so the overlap is an interval integrated with three-point
Gauss–Legendre (exact for the degree-3 Φ_a N_b r of the axisymmetric patch test). The
out-of-plane measure (the thickness, or 2πr at the secondary point) is in the weights, so A_A,
g̃_A and the forces are totals over the slab or the ring, as the 2D elements' are (B. Yang,
T. A. Laursen, X. Meng, IJNME 62:1183–1225, 2005). Points keep z = 0 and the same 24 variables
as faces, and the tangent blocks keep their in-plane rows. The element's thickness under an edge
is its area over the edge's length.
**Segmentation** (`fieldcraft_fem::mortar`): both faces are projected along the secondary
face's unit normal onto its plane, the main polygon is clipped by the secondary one
@@ -540,7 +552,9 @@ entries, and the results display draws `contact_pressure`, `contact_gap`, `conta
`contact_shear` and `contact_slip`.
**Verification** (`crates/solver/tests/contact.rs`, `tests/oracle/nl-contact-patch`,
`tests/oracle/nl-hertz-cylinder`): the contact patch test on non-matching hex8 and tet4 meshes
`tests/oracle/nl-hertz-cylinder`, and their 2D forms `nl-contact-patch-plane-strain`,
`nl-contact-patch-axisymmetric`, `nl-hertz-cylinder-plane-strain`): the contact patch test on
non-matching hex8, tet4 and (plane strain and axisymmetric) quad4 meshes
(uniform σ_zz, pressure equal to it, no lateral stress: to 10⁻⁹ with small sliding, 10⁻⁴ with
finite sliding, where the gaps the augmentation leaves enter through their geometric terms);
Hertz's line contact, plane strain (peak pressure within 0.3%, the pressure at two nodes within
@@ -550,7 +564,10 @@ Newton's quadratic rate near the solution with finite sliding; Coulomb friction
blocks (stick well below the cone with the forces of glued blocks, slip on it carrying μ N to 0.1%
with small sliding and 3% with finite sliding, where the block turns a little, and μ_k N once
slipping; `tests/oracle/nl-friction-sliding-blocks`); initial overclosure adjusted and
refused; bit-for-bit results on one and four threads; the refusals.
refused; bit-for-bit results on one and four threads; the refusals. In 2D: the patch tests pass
to 10⁻⁹ (small sliding) and 10⁻⁴ (finite), Hertz's line contact on quad4 edges matches the
hex8 model to 10⁻⁵, and a hybrid Mooney–Rivlin C-ring closing on itself in plane strain moves as
the one-layer solid ring with z held, node for node to 10⁻⁵.
## Ties (non-matching meshes)
+43 -1
View File
@@ -2,7 +2,7 @@
Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the policy is [verification.md](verification.md). Every case is our own model, built with FieldCraft commands from the problem's published description, and compared with the reference value its `expected.json` cites, within the stated tolerance and for the stated reason. Accepted results are frozen in each case's `fixture.json`: a later change that moves one beyond its band (10⁻⁹ of its size) fails CI even while the reference still passes. No reference value was produced with a commercial product.
**232 of 238 quantities pass** (6 known failure(s)) in 66 active cases: 57 theory, 9 published benchmark, 0 experiment; 10 pending cases wait for capabilities still being built.
**270 of 276 quantities pass** (6 known failure(s)) in 70 active cases: 61 theory, 9 published benchmark, 0 experiment; 10 pending cases wait for capabilities still being built.
## Theory: closed-form solutions, patch tests and mesh invariants
@@ -95,6 +95,36 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Finite: the contact pressure at the secondary node (0.5, 0.5, 1) over −σ_zz of element 19 | 1 | 0.99998 | -0.00172% | 0.02% | pass | matches |
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Finite: the integrated contact pressure over the reaction at the pushed top | 1 | 1 | < 1e-7% | 0.02% | pass | matches |
| [nl-contact-patch](../tests/oracle/nl-contact-patch/) | Small: σ_zz of element 19 (uniaxial stress E ε = −10 for the 1% squeeze) | -10 | -9.9996 | 0.00426% | 0.01% | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: σ_yy of element 4 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: σ_yy of element 6 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: σ_yy of element 1 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: σ_yy of element 10 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: σ_xx of element 6 over its σ_yy (uniaxial stress: 0) | 0 | ≈ 0 | ≈ 0 | ± 1.0000e-6 | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: the contact pressure at the secondary node (1.5, 1) over −σ_yy of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: the integrated contact pressure over the reaction at the pushed top | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: σ_yy of element 4 over that of element 7 | 1 | 1 | 0.0000126% | 0.02% | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: σ_yy of element 6 over that of element 7 | 1 | 0.99997 | -0.00325% | 0.02% | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: σ_yy of element 1 over that of element 7 | 1 | 0.99999 | -0.00149% | 0.02% | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: σ_yy of element 10 over that of element 7 | 1 | 0.99998 | -0.00185% | 0.02% | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: σ_xx of element 6 over its σ_yy (uniaxial stress: 0) | 0 | -6.2023e-6 | -6.2e-6 | ± 2.0000e-4 | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: the contact pressure at the secondary node (1.5, 1) over −σ_yy of element 7 | 1 | 0.99997 | -0.00285% | 0.02% | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Finite: the integrated contact pressure over the reaction at the pushed top | 1 | 1 | < 1e-7% | 0.02% | pass | matches |
| [nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/) | Small: σ_yy of element 7 (uniaxial stress E ε = −10 for the 1% squeeze) | -10 | -9.9996 | 0.00426% | 0.01% | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: σ_yy of element 4 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: σ_yy of element 6 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: σ_yy of element 1 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: σ_yy of element 10 over that of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: σ_xx of element 6 over its σ_yy (uniaxial stress: 0) | 0 | ≈ 0 | ≈ 0 | ± 1.0000e-6 | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: the contact pressure at the secondary node (0.5, 1) over −σ_yy of element 7 | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: the integrated contact pressure over the reaction at the pushed top | 1 | 1 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: σ_yy of element 4 over that of element 7 | 1 | 1 | 0.0000708% | 0.02% | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: σ_yy of element 6 over that of element 7 | 1 | 0.99999 | -0.00087% | 0.02% | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: σ_yy of element 1 over that of element 7 | 1 | 1 | -0.000312% | 0.02% | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: σ_yy of element 10 over that of element 7 | 1 | 1 | -0.000428% | 0.02% | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: σ_xx of element 6 over its σ_yy (uniaxial stress: 0) | 0 | ≈ 0 | ≈ 0 | ± 2.0000e-4 | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: the contact pressure at the secondary node (0.5, 1) over −σ_yy of element 7 | 1 | 0.99999 | -0.00075% | 0.02% | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Finite: the integrated contact pressure over the reaction at the pushed top | 1 | 1 | < 1e-7% | 0.02% | pass | matches |
| [nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/) | Small: σ_yy of element 7 (plane strain uniaxial stress E ε/(1 − ν²) = −10.989 for the 1% squeeze) | -10.989 | -10.988 | 0.00516% | 0.01% | pass | matches |
| [nl-cylinder-inflation](../tests/oracle/nl-cylinder-inflation/) | radial displacement of the bore (10, 0, 0) at the pressure that inflates it to a = 15 | 5 | 5.004 | 0.0793% | 0.5% | pass | matches |
| [nl-cylinder-inflation](../tests/oracle/nl-cylinder-inflation/) | radial displacement of the outer wall (20, 0, 0) | 2.9129 | 2.9173 | 0.152% | 0.5% | pass | matches |
| [nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/) | tip deflection δ_v/L at P L²/(E I) = 1 | 0.30172 | 0.30226 | 0.179% | 1% | pass | matches |
@@ -114,6 +144,12 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
| [nl-hertz-cylinder](../tests/oracle/nl-hertz-cylinder/) | the contact pressure at the secondary node at x = 0.36701 (node 8) | 1959 | 2078.4 | 6.09% | 10% | pass | matches |
| [nl-hertz-cylinder](../tests/oracle/nl-hertz-cylinder/) | the contact pressure at the secondary node at x = 0.63325 (node 11, beyond the half-width b = 0.5) | 0 | ≈ 0 | ≈ 0 | ± 1 | pass | matches |
| [nl-hertz-cylinder](../tests/oracle/nl-hertz-cylinder/) | the vertical stress σ_yy under the centre of contact at depth 0.47127 (node 713) | -2099.2 | -2059.8 | 1.87% | 4% | pass | matches |
| [nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/) | the largest contact pressure p₀ (at x = 0) | 2884.6 | 2892 | 0.255% | 2% | pass | matches |
| [nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/) | the force across the contact (half model, 1 mm thick) | 1132.8 | 1133 | 0.0228% | 0.2% | pass | matches |
| [nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/) | the contact pressure at the secondary node at x = 0.29594 | 2325.1 | 2257.9 | -2.89% | 5% | pass | matches |
| [nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/) | the contact pressure at the secondary node at x = 0.36701 | 1959 | 2078.4 | 6.09% | 10% | pass | matches |
| [nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/) | the contact pressure at the secondary node at x = 0.63325 (beyond the half-width b = 0.5) | 0 | ≈ 0 | ≈ 0 | ± 1 | pass | matches |
| [nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/) | the vertical stress σ_yy under the centre of contact at depth 0.47127 | -2099.2 | -2059.8 | 1.87% | 4% | pass | matches |
| [nl-hybrid-incompressible](../tests/oracle/nl-hybrid-incompressible/) | axial Cauchy stress, stretch λ = 2, ν = 0.4999 | 3.4988 | 3.4988 | 0.000000135% | 1e-6 rel. | pass | matches |
| [nl-hybrid-incompressible](../tests/oracle/nl-hybrid-incompressible/) | lateral displacement at (1, 1, 1): l − 1 | -0.29281 | -0.29281 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-j2-uniaxial](../tests/oracle/nl-j2-uniaxial/) | axial Cauchy stress at a logarithmic strain of 0.2 | 642.74 | 642.74 | < 1e-7% | 1e-6 rel. | pass | matches |
@@ -125,6 +161,8 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
| [nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/) | Cauchy stress across, in the held direction, pure shear λ = 2 | 1.5067 | 1.5067 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-mooney-rivlin-tube](../tests/oracle/nl-mooney-rivlin-tube/) | radial displacement of the bore (10, 0, 0) at the pressure that inflates it to a = 15 at λ_z = 1.5 | 5 | 5.0311 | 0.622% | 1% | pass | matches |
| [nl-mooney-rivlin-tube](../tests/oracle/nl-mooney-rivlin-tube/) | radial displacement of the outer wall (20, 0, 0): b − B, a small difference of radii | 0.61553 | 0.64339 | 2.8e-2 | ± 0.04 | pass | matches |
| [nl-mooney-rivlin-tube-axisymmetric](../tests/oracle/nl-mooney-rivlin-tube-axisymmetric/) | radial displacement of the bore (10, 0, 0) at the pressure that inflates it to a = 15 at λ_z = 1.5 | 5 | 5.0329 | 0.658% | 1% | pass | matches |
| [nl-mooney-rivlin-tube-axisymmetric](../tests/oracle/nl-mooney-rivlin-tube-axisymmetric/) | radial displacement of the outer wall (20, 0, 0): b − B, a small difference of radii | 0.61553 | 0.64473 | 2.9e-2 | ± 0.04 | pass | matches |
| [nl-neo-hookean-uniaxial](../tests/oracle/nl-neo-hookean-uniaxial/) | axial Cauchy stress at the corner (1, 1, 1), stretch λ = 2 | 8.0601 | 8.0601 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-neo-hookean-uniaxial](../tests/oracle/nl-neo-hookean-uniaxial/) | lateral displacement u_y at the corner (1, 1, 1), l − 1 | -0.18686 | -0.18686 | < 1e-7% | 1e-6 rel. | pass | matches |
| [nl-patch-hex8](../tests/oracle/nl-patch-hex8/) | u_x at inner node 22 | 0.13583 | 0.13583 | < 1e-7% | 1e-8 rel. | pass | matches |
@@ -291,14 +329,18 @@ Generated by `cargo xtask oracle` from [`tests/oracle/`](../tests/oracle/); the
- **[nafems-le1-plane-stress](../tests/oracle/nafems-le1-plane-stress/)** (benchmark): NAFEMS LE1 elliptic membrane with 2D plane stress elements (quad8). Source: NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE1 (target σ_yy = 92.7 MPa at D); as reproduced in the Abaqus 2017 Benchmarks Guide, 'LE1: Plane stress elements—elliptic membrane'. Tolerances — syy_D: the target is published to three figures; the stress extrapolated to the edge on a converged mesh adds a few tenths of a percent
- **[nafems-le10](../tests/oracle/nafems-le10/)** (benchmark): NAFEMS LE10 thick plate under pressure (hex20). Source: NAFEMS, The Standard NAFEMS Benchmarks, TNSB Rev. 3, October 1990, test LE10 (target σ_yy = −5.38 MPa at D); as reproduced in the Abaqus 2017 Benchmarks Guide, 'LE10: Thick plate under pressure'. Tolerances — syy_D: the target is published to three figures; at D the stress is extrapolated to a corner where the hole edge meets the loaded face, and published converged solutions scatter by 1–2% (Abaqus Benchmarks Guide fine meshes: −5.51 to −5.89)
- **[nl-contact-patch](../tests/oracle/nl-contact-patch/)** (theory): Contact patch test: two blocks with non-matching meshes in uniaxial compression (implicit mortar contact). Source: The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; Crisfield, Re-visiting the contact patch test, IJNME 48:435–449, 2000; passed by mortar methods: Puso & Laursen, CMAME 193:601–629, 2004. Uniaxial stress between frictionless symmetry planes is uniform in both blocks. Tolerances — small_lower_corner: small sliding is linear and its mortar integrals exact on these rectangular faces: a uniform stress is the discrete solution up to the convergence tolerance (10⁻¹⁰); small_lower_centre: as for the corner element; small_lower_base: as for the corner element; small_upper_top: as for the corner element; small_lateral: frictionless symmetry planes and a frictionless interface leave the blocks free to expand sideways; the non-matching interface must not hold them; small_pressure: the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface; small_force: the pressure carries the whole load across the interface; finite_lower_corner: finite sliding is exact up to the geometric terms of the gaps the augmentation leaves (10⁻⁵ of the element thickness) and the default convergence tolerances; finite_lower_centre: as for the corner element; finite_lower_base: as for the corner element; finite_upper_top: as for the corner element; finite_lateral: frictionless symmetry planes and a frictionless interface leave the blocks free to expand sideways; the non-matching interface must not hold them; finite_pressure: the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface; finite_force: the pressure carries the whole load across the interface; small_stress: the squeeze less the penetration the augmented Lagrangian leaves (up to 10⁻⁵ of the element thickness, a few 10⁻⁵ of the squeeze)
- **[nl-contact-patch-axisymmetric](../tests/oracle/nl-contact-patch-axisymmetric/)** (theory): Contact patch test, axisymmetric: two rings with non-matching edges in axial compression (implicit mortar contact). Source: The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; passed by 2D mortar methods: Yang, Laursen & Meng, IJNME 62:1183–1225, 2005. Uniaxial stress between frictionless supports is uniform in both blocks. Tolerances — small_lower_corner: small sliding is linear and its edge mortar integrals exact (degree-3 Gauss on the overlaps): the uniform stress is the discrete solution up to the convergence tolerance; small_lower_far: as for the corner element; small_lower_base: as for the corner element; small_upper_top: as for the corner element; small_lateral: a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them; small_pressure: the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface; small_force: the pressure carries the whole load across the interface (both are totals over the circumference); finite_lower_corner: finite sliding is exact up to the geometric terms of the gaps the augmentation leaves (10⁻⁵ of the element thickness) and the default convergence tolerances; finite_lower_far: as for the corner element; finite_lower_base: as for the corner element; finite_upper_top: as for the corner element; finite_lateral: a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them; finite_pressure: the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface; finite_force: the pressure carries the whole load across the interface (both are totals over the circumference); small_stress: the squeeze less the penetration the augmented Lagrangian leaves (up to 10⁻⁵ of the element thickness)
- **[nl-contact-patch-plane-strain](../tests/oracle/nl-contact-patch-plane-strain/)** (theory): Contact patch test in plane strain: two blocks with non-matching edges in uniaxial compression (implicit mortar contact). Source: The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; passed by 2D mortar methods: Yang, Laursen & Meng, IJNME 62:1183–1225, 2005. Uniaxial stress between frictionless supports is uniform in both blocks. Tolerances — small_lower_corner: small sliding is linear and its edge mortar integrals exact (degree-3 Gauss on the overlaps): the uniform stress is the discrete solution up to the convergence tolerance; small_lower_far: as for the corner element; small_lower_base: as for the corner element; small_upper_top: as for the corner element; small_lateral: a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them; small_pressure: the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface; small_force: the pressure carries the whole load across the interface (both are totals over the thickness); finite_lower_corner: finite sliding is exact up to the geometric terms of the gaps the augmentation leaves (10⁻⁵ of the element thickness) and the default convergence tolerances; finite_lower_far: as for the corner element; finite_lower_base: as for the corner element; finite_upper_top: as for the corner element; finite_lateral: a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them; finite_pressure: the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface; finite_force: the pressure carries the whole load across the interface (both are totals over the thickness); small_stress: the squeeze less the penetration the augmented Lagrangian leaves (up to 10⁻⁵ of the element thickness)
- **[nl-cylinder-inflation](../tests/oracle/nl-cylinder-inflation/)** (theory): Inflation of a thick neo-Hookean cylinder by a follower pressure (plane strain). Source: Finite inflation of an incompressible neo-Hookean thick-walled tube in plane strain: r² = R² + a² − A², σ_θ − σ_r = μ (λ² − λ⁻²) with λ = r/R, integrated through the wall: p = (μ/2)[(ln λ_a² − λ_a⁻²) − (ln λ_b² − λ_b⁻²)] (R. S. Rivlin, Large elastic deformations of isotropic materials VI, Phil. Trans. R. Soc. A 242:173–195, 1949; R. W. Ogden, Non-Linear Elastic Deformations, Dover 1997, §5.3.2). Tolerances — ua: the closed form is incompressible and our material nearly so (κ/μ = 1000, a volume change of order p/κ ≈ 4·10⁻⁴); hybrid hex20, 6 × 16, follow the wall's strain gradient well inside; ub: as at the bore
- **[nl-elastica-cantilever](../tests/oracle/nl-elastica-cantilever/)** (theory): Large-deflection cantilever under a dead tip load against the elastica (solids). Source: The elastica of a cantilever under a vertical end load: K. E. Bisshopp, D. C. Drucker, Large deflection of cantilever beams, Quarterly of Applied Mathematics 3(3):272–275, 1945 (elliptic-integral solution); values computed by integrating θ″ = −(P/EI) cos θ by shooting (our own RK4, 20000 steps), agreeing with the table of K. Mattiasson, Numerical results from large deflection beam and frame problems analysed by means of elliptic integrals, IJNME 17(1):145–153, 1981, to all six digits. Tolerances — v1: hex20, 40 × 2 through the depth, L/h = 20: shear deformation (0.6 (h/L)² ≈ 0.15 % of the deflection) and the clamp's local restraint, both absent from the elastica; u1: as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's; v2: hex20, 40 × 2 through the depth, L/h = 20: shear deformation (0.6 (h/L)² ≈ 0.15 % of the deflection) and the clamp's local restraint, both absent from the elastica; u2: as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's; v5: hex20, 40 × 2 through the depth, L/h = 20: shear deformation (0.6 (h/L)² ≈ 0.15 % of the deflection) and the clamp's local restraint, both absent from the elastica; u5: as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's; v10: hex20, 40 × 2 through the depth, L/h = 20: shear deformation (0.6 (h/L)² ≈ 0.15 % of the deflection) and the clamp's local restraint, both absent from the elastica; u10: as δ_v; the shortening is the smaller, second-order quantity, so its relative error is about twice the deflection's
- **[nl-friction-sliding-blocks](../tests/oracle/nl-friction-sliding-blocks/)** (theory): Coulomb friction: a block pressed on another and slid (implicit mortar contact). Source: Coulomb's law of dry friction: a sliding interface carries μ_k times the normal force against the slip (C. A. Coulomb, Théorie des machines simples, Mém. Math. Phys. Acad. Sci. 10:161–332, 1785; P. Wriggers, Computational Contact Mechanics, 2nd ed., Springer 2006, §5.2). Tolerances — kinetic: once the interface slips its friction is μ_k N exactly (the return map puts every slipping node's traction on the cone); the force opposes the upper block's motion; balance: the friction carries the whole sideways load; slipping: a sideways demand far beyond μ N slips the whole interface
- **[nl-hertz-cylinder](../tests/oracle/nl-hertz-cylinder/)** (theory): Hertz line contact: a cylinder pressed on a flat (implicit mortar contact, plane strain). Source: H. Hertz, Über die Berührung fester elastischer Körper, J. reine angew. Math. 92:156–171, 1882; K. L. Johnson, Contact Mechanics, Cambridge University Press 1985, §4.3 (two-dimensional contact of cylindrical bodies, eqs. 4.45–4.49): b = √(4PR/(πE*)), p₀ = 2P/(πb), with 1/E* = 2(1 − ν²)/E for two bodies of one material and R the cylinder's radius on a flat. Tolerances — peak_pressure: p₀ = 2P/(πb) for the load per length P = 2265.57 N/mm, b = 0.5 mm; the contact width spans seven elements of the graded mesh; normal_force: the pressure carries the whole load; the soft springs that hold the indenter before contact take a millionth; pressure_x0296: p₀ √(1 − x²/b²) at the node; the nodal pressure (the node's normal force over the area it carries) averages the distribution over the node's elements; pressure_x0367: as at node 7, nearer the edge, where the elements round off the square-root fall of the pressure to zero at b; pressure_x0633: outside the contact the surfaces are apart: no pressure (an element beyond b, since the edge element carries part of the last closed node's pressure); syy_depth: σ_z = −p₀ / √(1 + z²/b²) on the axis of a half-plane (Johnson 1985, eq. 4.49); the foundation is ten half-widths wide and deep, not a half-plane, and the stress is the nodal average of the elements around the node
- **[nl-hertz-cylinder-plane-strain](../tests/oracle/nl-hertz-cylinder-plane-strain/)** (theory): Hertz line contact: a cylinder pressed on a flat (implicit mortar contact of 2D edges, plane strain). Source: H. Hertz, Über die Berührung fester elastischer Körper, J. reine angew. Math. 92:156–171, 1882; K. L. Johnson, Contact Mechanics, Cambridge University Press 1985, §4.3 (two-dimensional contact of cylindrical bodies, eqs. 4.45–4.49): b = √(4PR/(πE*)), p₀ = 2P/(πb), with 1/E* = 2(1 − ν²)/E for two bodies of one material and R the cylinder's radius on a flat. Tolerances — peak_pressure: p₀ = 2P/(πb) for the load per length P = 2265.57 N/mm, b = 0.5 mm; the contact width spans seven elements of the graded mesh; normal_force: the pressure carries the whole load; the soft springs that hold the indenter before contact take a millionth; pressure_x0296: p₀ √(1 − x²/b²) at the node; the nodal pressure (the node's normal force over the area it carries) averages the distribution over the node's elements; pressure_x0367: as at node 7, nearer the edge, where the elements round off the square-root fall of the pressure to zero at b; pressure_x0633: outside the contact the surfaces are apart: no pressure (an element beyond b, since the edge element carries part of the last closed node's pressure); syy_depth: σ_z = −p₀ / √(1 + z²/b²) on the axis of a half-plane (Johnson 1985, eq. 4.49); the foundation is ten half-widths wide and deep, not a half-plane, and the stress is the nodal average of the elements around the node
- **[nl-hybrid-incompressible](../tests/oracle/nl-hybrid-incompressible/)** (theory): Nearly incompressible hybrid block under prescribed stretch. Source: Uniaxial stress of the decoupled compressible neo-Hookean energy W = μ/2 (Ī₁ − 3) + κ/2 (J − 1)² (P. J. Flory, Trans. Faraday Soc. 57:829–838, 1961): σ = μ J^(−5/3) (λ² − I₁/3) + κ (J − 1) with the lateral stretch found so σ_yy = 0 (bisection; E = 3, ν = 0.4999: μ = 1.0000667, κ = 5000). Tolerances — sigma: a homogeneous deformation every hybrid hex8 represents exactly; the 27-element mesh makes the global mixed tangent indefinite in the first iterations (LDLᵀ): the tolerance admits the Newton tolerance and rounding; lateral: a homogeneous deformation every hybrid hex8 represents exactly; the 27-element mesh makes the global mixed tangent indefinite in the first iterations (LDLᵀ): the tolerance admits the Newton tolerance and rounding
- **[nl-j2-uniaxial](../tests/oracle/nl-j2-uniaxial/)** (theory): Uniaxial tension of a J2 bilinear-hardening cube to large strain (implicit, finite strain). Source: J2 plasticity with linear isotropic hardening in uniaxial stress (Simo and Hughes, Computational Inelasticity, Springer 1998, §2.3) in the logarithmic strain, which Simo's exponential return map (CMAME 99:61–112, 1992) integrates exactly on a proportional path: Kirchhoff stress τ = σ_y + E_t (ε − σ_y/E), Cauchy stress σ = τ/J with ln J = τ/(3K) (Hencky's volume law). Tolerances — sigma: a homogeneous deformation the element represents exactly, reached on a proportional path: the tolerance only admits the Newton tolerance and rounding; plastic_strain: ε_p = (τ − σ_y)/H, as for the stress
- **[nl-mooney-rivlin-modes](../tests/oracle/nl-mooney-rivlin-modes/)** (theory): Mooney–Rivlin block in uniaxial, equibiaxial and pure-shear tension. Source: Treloar's homogeneous modes of rubber testing (L. R. G. Treloar, The Physics of Rubber Elasticity, 3rd ed., Oxford 1975, ch. 10) for the decoupled compressible energy W = C₁₀ (Ī₁ − 3) + C₀₁ (Ī₂ − 3) + κ/2 (J − 1)²: principal Cauchy stresses σᵢ = (λᵢ/J) ∂W/∂λᵢ with the free stretch found so the free faces carry no stress (bisection; E = 3, ν = 0.49, C₀₁ = 0.2, C₁₀ = μ/2 − C₀₁). Tolerances — uniaxial: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; uniaxial_lateral: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; equibiaxial: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; shear_xx: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding; shear_yy: a homogeneous deformation the hybrid hex20 represents exactly: the tolerance admits the Newton tolerance and rounding
- **[nl-mooney-rivlin-tube](../tests/oracle/nl-mooney-rivlin-tube/)** (theory): Inflation of a stretched Mooney–Rivlin tube (follower pressure). Source: Finite inflation and extension of an incompressible Mooney–Rivlin tube (R. S. Rivlin, Large elastic deformations of isotropic materials VI, Phil. Trans. R. Soc. A 242:173–195, 1949; R. W. Ogden, Non-Linear Elastic Deformations, Dover 1997, §5.3): at axial stretch λ_z, r² = a² + (R² − A²)/λ_z, σ_θ − σ_r = 2 (λ_θ² − λ_r²)(C₁₀ + C₀₁ λ_z²) with λ_θ = r/R, λ_r = 1/(λ_θ λ_z), integrated through the wall: p = ∫ (σ_θ − σ_r) dr/r (Simpson, 2·10⁵ intervals). Tolerances — ua: 16 × 6 hybrid hex20 around the quarter; ν = 0.4995 stands in for incompressibility (a volume change of order p/κ ≈ 10⁻³ shifts the radii by about 0.1 %), and the tube is stretched axially by its ends, which a nearly incompressible solid takes with larger pressures than the plane-strain neo-Hookean tube; ub: 0.2 % of the outer radius b = 20.616: the same volume change of order 10⁻³ moves b by about 0.03, which is 4.5 % of b − B; the incompressible kinematics r² = a² + (R² − A²)/λ_z amplify it there
- **[nl-mooney-rivlin-tube-axisymmetric](../tests/oracle/nl-mooney-rivlin-tube-axisymmetric/)** (theory): Inflation of a stretched Mooney–Rivlin tube (axisymmetric hybrid quad4). Source: Finite inflation and extension of an incompressible Mooney–Rivlin tube (R. S. Rivlin, Large elastic deformations of isotropic materials VI, Phil. Trans. R. Soc. A 242:173–195, 1949; R. W. Ogden, Non-Linear Elastic Deformations, Dover 1997, §5.3): at axial stretch λ_z, r² = a² + (R² − A²)/λ_z, σ_θ − σ_r = 2 (λ_θ² − λ_r²)(C₁₀ + C₀₁ λ_z²) with λ_θ = r/R, λ_r = 1/(λ_θ λ_z), integrated through the wall: p = ∫ (σ_θ − σ_r) dr/r (Simpson, 2·10⁵ intervals). Tolerances — ua: 16 × 6 hybrid hex20 around the quarter; ν = 0.4995 stands in for incompressibility (a volume change of order p/κ ≈ 10⁻³ shifts the radii by about 0.1 %), and the tube is stretched axially by its ends, which a nearly incompressible solid takes with larger pressures than the plane-strain neo-Hookean tube; ub: 0.2 % of the outer radius b = 20.616: the same volume change of order 10⁻³ moves b by about 0.03, which is 4.5 % of b − B; the incompressible kinematics r² = a² + (R² − A²)/λ_z amplify it there
- **[nl-neo-hookean-uniaxial](../tests/oracle/nl-neo-hookean-uniaxial/)** (theory): Uniaxial stretch of a neo-Hookean block to twice its length. Source: Closed form of the compressible neo-Hookean model W = μ/2 (Ī₁ − 3) + κ/2 (J − 1)² in uniaxial stress (G. A. Holzapfel, Nonlinear Solid Mechanics, Wiley 2000, §6.5, eq. 6.148 with the volumetric term of §6.4): σ_yy = μ J^(−5/3)(l² − Ī/3) + κ(J − 1) = 0 fixes the lateral stretch l, then σ_xx = μ J^(−5/3)(λ² − l²). Tolerances — sigma: a homogeneous deformation every element represents exactly: the tolerance only admits rounding and the Newton tolerance; lateral: a homogeneous deformation every element represents exactly: the tolerance only admits rounding and the Newton tolerance
- **[nl-patch-hex8](../tests/oracle/nl-patch-hex8/)** (theory): Nonlinear patch test, hex8: a homogeneous finite deformation of a distorted mesh. Source: The patch test (B. M. Irons, A. Razzaque, in A. K. Aziz (ed.), The Mathematical Foundations of the Finite Element Method, Academic Press 1972, 557–587) at finite strain: a boundary moved by x = F X makes F the solution everywhere (J. Bonet, R. D. Wood, Nonlinear Continuum Mechanics for Finite Element Analysis, 2nd ed., Cambridge 2008, §8); the neo-Hookean Cauchy stress σ = μ J^(−5/3) dev b + κ (J − 1) I of that F. Tolerances — ux: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; uy: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; uz: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; sxx: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; syy: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; szz: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; sxy: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; sxz: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance; syz: a patch test is passed exactly: the tolerance admits rounding and the Newton tolerance
- **[nl-rigid-rotation](../tests/oracle/nl-rigid-rotation/)** (theory): A quarter turn strains and stresses nothing (objectivity). Source: Objectivity of the total Lagrangian formulation and of the hyperelastic stress: a rigid motion x = R X + c gives C = I and σ = 0 whatever the rotation (Bonet & Wood 2008, §3.4 and §5.2; T. Belytschko, W. K. Liu, B. Moran, Nonlinear Finite Elements for Continua and Structures, 2nd ed., Wiley 2014, §5.5). Tolerances — ux: the inner nodes follow the rigid rotation exactly; uy: the inner nodes follow the rigid rotation exactly; uz: the inner nodes follow the rigid rotation exactly; von_mises: zero up to rounding of stresses of order E = 1000: a 90° turn leaves no strain
@@ -0,0 +1,11 @@
# Contact patch test, axisymmetric (implicit mortar contact, non-matching edges)
The axisymmetric form of `nl-contact-patch`: two rings of unit square section, 1 ≤ r ≤ 2, stacked
along the axis y and meshed independently (3 × 2 quad4 below, graded in r; 2 × 2 above), so the
nodes of the interface at y = 1 do not match. A frictionless contact pair joins the lower ring's
top edge (main) and the upper ring's bottom edge (secondary). The base y = 0 is held in y and
the top y = 2 pushed down by 0.02 (E = 1000, ν = 0.3), in a `Small` (small sliding) and a `Finite`
(finite sliding) nonlinear static step. Axial compression with free radial expansion is a
uniform uniaxial stress σ_yy in both rings (u_r = −ν ε r, so the hoop strain equals the radial
one), carried by a uniform contact pressure; the edge mortar integrals weight by 2πr, so the
patch test checks that the radius enters the gaps and the forces consistently.
@@ -0,0 +1,432 @@
{
"title": "Contact patch test, axisymmetric: two rings with non-matching edges in axial compression (implicit mortar contact)",
"kind": "theory",
"source": "The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; passed by 2D mortar methods: Yang, Laursen & Meng, IJNME 62:1183–1225, 2005. Uniaxial stress between frictionless supports is uniform in both blocks",
"quantities": [
{
"id": "small_lower_corner",
"quantity": "Small: σ_yy of element 4 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
4
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 1e-06
},
"reason": "small sliding is linear and its edge mortar integrals exact (degree-3 Gauss on the overlaps): the uniform stress is the discrete solution up to the convergence tolerance"
},
{
"id": "small_lower_far",
"quantity": "Small: σ_yy of element 6 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
6
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 1e-06
},
"reason": "as for the corner element"
},
{
"id": "small_lower_base",
"quantity": "Small: σ_yy of element 1 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
1
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 1e-06
},
"reason": "as for the corner element"
},
{
"id": "small_upper_top",
"quantity": "Small: σ_yy of element 10 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
10
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 1e-06
},
"reason": "as for the corner element"
},
{
"id": "small_lateral",
"quantity": "Small: σ_xx of element 6 over its σ_yy (uniaxial stress: 0)",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
6
]
},
"pointer": "/elements/0/stress/0/0",
"over": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
6
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 0.0,
"tolerance": {
"absolute": 1e-06
},
"reason": "a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them"
},
{
"id": "small_pressure",
"quantity": "Small: the contact pressure at the secondary node (1.5, 1) over −σ_yy of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"at": [
[
1.5,
1.0,
0.0
]
]
},
"pointer": "/nodes/0/contact/0/pressure",
"over": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1",
"scale": -1.0
}
},
"value": 1.0,
"tolerance": {
"relative": 1e-06
},
"reason": "the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface"
},
{
"id": "small_force",
"quantity": "Small: the integrated contact pressure over the reaction at the pushed top",
"measure": {
"from": "small",
"pointer": "/contacts/0/normal_force",
"over": {
"command": "results.reactions",
"params": {
"step": "Small",
"sets": [
"top"
]
},
"pointer": "/sets/0/force/1",
"scale": -1.0
}
},
"value": 1.0,
"tolerance": {
"relative": 1e-06
},
"reason": "the pressure carries the whole load across the interface (both are totals over the circumference)"
},
{
"id": "finite_lower_corner",
"quantity": "Finite: σ_yy of element 4 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
4
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 0.0002
},
"reason": "finite sliding is exact up to the geometric terms of the gaps the augmentation leaves (10⁻⁵ of the element thickness) and the default convergence tolerances"
},
{
"id": "finite_lower_far",
"quantity": "Finite: σ_yy of element 6 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
6
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 0.0002
},
"reason": "as for the corner element"
},
{
"id": "finite_lower_base",
"quantity": "Finite: σ_yy of element 1 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
1
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 0.0002
},
"reason": "as for the corner element"
},
{
"id": "finite_upper_top",
"quantity": "Finite: σ_yy of element 10 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
10
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 0.0002
},
"reason": "as for the corner element"
},
{
"id": "finite_lateral",
"quantity": "Finite: σ_xx of element 6 over its σ_yy (uniaxial stress: 0)",
"measure": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
6
]
},
"pointer": "/elements/0/stress/0/0",
"over": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
6
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 0.0,
"tolerance": {
"absolute": 0.0002
},
"reason": "a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them"
},
{
"id": "finite_pressure",
"quantity": "Finite: the contact pressure at the secondary node (1.5, 1) over −σ_yy of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Finite",
"at": [
[
1.5,
1.0,
0.0
]
]
},
"pointer": "/nodes/0/contact/0/pressure",
"over": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1",
"scale": -1.0
}
},
"value": 1.0,
"tolerance": {
"relative": 0.0002
},
"reason": "the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface"
},
{
"id": "finite_force",
"quantity": "Finite: the integrated contact pressure over the reaction at the pushed top",
"measure": {
"from": "finite",
"pointer": "/contacts/0/normal_force",
"over": {
"command": "results.reactions",
"params": {
"step": "Finite",
"sets": [
"top"
]
},
"pointer": "/sets/0/force/1",
"scale": -1.0
}
},
"value": 1.0,
"tolerance": {
"relative": 0.0002
},
"reason": "the pressure carries the whole load across the interface (both are totals over the circumference)"
},
{
"id": "small_stress",
"quantity": "Small: σ_yy of element 7 (uniaxial stress E ε = −10 for the 1% squeeze)",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
},
"value": -10.0,
"tolerance": {
"relative": 0.0001
},
"reason": "the squeeze less the penetration the augmented Lagrangian leaves (up to 10⁻⁵ of the element thickness)"
}
]
}
@@ -0,0 +1,65 @@
{
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
"values": {
"finite_force": {
"band": 1.000000000000021e-9,
"value": 1.0000000000000209
},
"finite_lateral": {
"band": 0.0002,
"value": -6.202292989414768e-6
},
"finite_lower_base": {
"band": 1e-9,
"value": 0.9999851288470675
},
"finite_lower_corner": {
"band": 1.0000001256632718e-9,
"value": 1.0000001256632718
},
"finite_lower_far": {
"band": 1e-9,
"value": 0.9999675186278648
},
"finite_pressure": {
"band": 1e-9,
"value": 0.999971468750515
},
"finite_upper_top": {
"band": 1e-9,
"value": 0.999981532712154
},
"small_force": {
"band": 1e-9,
"value": 0.9999999999999044
},
"small_lateral": {
"band": 1e-6,
"value": 3.4213832901728455e-15
},
"small_lower_base": {
"band": 1.0000000000000207e-9,
"value": 1.0000000000000207
},
"small_lower_corner": {
"band": 1e-9,
"value": 0.9999999999999943
},
"small_lower_far": {
"band": 1.0000000000000418e-9,
"value": 1.0000000000000417
},
"small_pressure": {
"band": 1e-9,
"value": 0.9999999999998981
},
"small_stress": {
"band": 1e-8,
"value": -9.999573821916737
},
"small_upper_top": {
"band": 1.0000000000000296e-9,
"value": 1.0000000000000295
}
}
}
@@ -0,0 +1,20 @@
[
{"command": "mesh.block", "params": {"body": "Lower", "kind": "quad4", "corners": [[1.0, 0, 0], [2.0, 0, 0], [2.0, 1, 0], [1.0, 1, 0]], "divisions": [3, 2], "bias": [2.0, 1.0], "edges": [{"corners": [1, 2], "name": "bottom"}, {"corners": [3, 4], "name": "top"}, {"corners": [4, 1], "name": "x0"}]}},
{"command": "mesh.block", "params": {"body": "Upper", "kind": "quad4", "corners": [[1.0, 1, 0], [2.0, 1, 0], [2.0, 2, 0], [1.0, 2, 0]], "divisions": [2, 2], "edges": [{"corners": [1, 2], "name": "bottom"}, {"corners": [3, 4], "name": "top"}, {"corners": [4, 1], "name": "x0"}], "merge": 0}},
{"command": "model.set.create", "params": {"name": "lower", "geometry": [{"body": "Lower", "face": "area"}]}},
{"command": "model.set.create", "params": {"name": "upper", "geometry": [{"body": "Upper", "face": "area"}]}},
{"command": "model.set.create", "params": {"name": "lower_top", "geometry": [{"body": "Lower", "edge": "top"}]}},
{"command": "model.set.create", "params": {"name": "upper_bottom", "geometry": [{"body": "Upper", "edge": "bottom"}]}},
{"command": "model.set.create", "params": {"name": "base", "geometry": [{"body": "Lower", "edge": "bottom"}]}},
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Upper", "edge": "top"}]}},
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}}},
{"command": "model.section.create", "params": {"name": "lower", "set": "lower", "material": "Mat", "kind": {"type": "axisymmetric"}}},
{"command": "model.section.create", "params": {"name": "upper", "set": "upper", "material": "Mat", "kind": {"type": "axisymmetric"}}},
{"command": "interaction.contact.pair", "params": {"name": "interface", "main": "lower_top", "secondary": "upper_bottom"}},
{"command": "model.boundary_condition.create", "params": {"name": "base", "type": "displacement", "target": {"set": "base"}, "values": [null, 0, null, null, null, null]}},
{"command": "model.boundary_condition.create", "params": {"name": "push", "type": "displacement", "target": {"set": "top"}, "values": [null, -0.02, null, null, null, null]}},
{"command": "model.step.create", "params": {"name": "Small", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false, "tolerances": {"force": 1e-10, "displacement": 1e-08, "energy": 1e-16}}, "boundary_conditions": ["base", "push"], "loads": []}},
{"command": "model.step.create", "params": {"name": "Finite", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["base", "push"], "loads": []}},
{"command": "solve.nonlinear", "params": {"step": "Small"}, "id": "small"},
{"command": "solve.nonlinear", "params": {"step": "Finite"}, "id": "finite"}
]
@@ -0,0 +1,10 @@
# Contact patch test in plane strain (implicit mortar contact, non-matching edges)
The 2D form of `nl-contact-patch`: two unit squares stacked along y in plane strain (2 mm thick),
meshed independently: 3 × 2 quad4 below (graded in x), 2 × 2 above, so the nodes of the interface
at y = 1 do not match. A frictionless contact pair joins the lower block's top edge (main) and
the upper block's bottom edge (secondary). The edge x = 0 is a symmetry line, the base y = 0 is
held in y, and the top y = 2 is pushed down by 0.02 (E = 1000, ν = 0.3) in two nonlinear static
steps: `Small` without nonlinear geometry (small sliding) and `Finite` with it (finite sliding).
The exact state is uniaxial stress σ_yy (σ_zz = ν σ_yy out of plane), uniform in both blocks,
with a uniform contact pressure equal to it.
@@ -0,0 +1,432 @@
{
"title": "Contact patch test in plane strain: two blocks with non-matching edges in uniaxial compression (implicit mortar contact)",
"kind": "theory",
"source": "The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; passed by 2D mortar methods: Yang, Laursen & Meng, IJNME 62:1183–1225, 2005. Uniaxial stress between frictionless supports is uniform in both blocks",
"quantities": [
{
"id": "small_lower_corner",
"quantity": "Small: σ_yy of element 4 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
4
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 1e-06
},
"reason": "small sliding is linear and its edge mortar integrals exact (degree-3 Gauss on the overlaps): the uniform stress is the discrete solution up to the convergence tolerance"
},
{
"id": "small_lower_far",
"quantity": "Small: σ_yy of element 6 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
6
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 1e-06
},
"reason": "as for the corner element"
},
{
"id": "small_lower_base",
"quantity": "Small: σ_yy of element 1 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
1
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 1e-06
},
"reason": "as for the corner element"
},
{
"id": "small_upper_top",
"quantity": "Small: σ_yy of element 10 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
10
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 1e-06
},
"reason": "as for the corner element"
},
{
"id": "small_lateral",
"quantity": "Small: σ_xx of element 6 over its σ_yy (uniaxial stress: 0)",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
6
]
},
"pointer": "/elements/0/stress/0/0",
"over": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
6
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 0.0,
"tolerance": {
"absolute": 1e-06
},
"reason": "a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them"
},
{
"id": "small_pressure",
"quantity": "Small: the contact pressure at the secondary node (0.5, 1) over −σ_yy of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"at": [
[
0.5,
1.0,
0.0
]
]
},
"pointer": "/nodes/0/contact/0/pressure",
"over": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1",
"scale": -1.0
}
},
"value": 1.0,
"tolerance": {
"relative": 1e-06
},
"reason": "the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface"
},
{
"id": "small_force",
"quantity": "Small: the integrated contact pressure over the reaction at the pushed top",
"measure": {
"from": "small",
"pointer": "/contacts/0/normal_force",
"over": {
"command": "results.reactions",
"params": {
"step": "Small",
"sets": [
"top"
]
},
"pointer": "/sets/0/force/1",
"scale": -1.0
}
},
"value": 1.0,
"tolerance": {
"relative": 1e-06
},
"reason": "the pressure carries the whole load across the interface (both are totals over the thickness)"
},
{
"id": "finite_lower_corner",
"quantity": "Finite: σ_yy of element 4 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
4
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 0.0002
},
"reason": "finite sliding is exact up to the geometric terms of the gaps the augmentation leaves (10⁻⁵ of the element thickness) and the default convergence tolerances"
},
{
"id": "finite_lower_far",
"quantity": "Finite: σ_yy of element 6 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
6
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 0.0002
},
"reason": "as for the corner element"
},
{
"id": "finite_lower_base",
"quantity": "Finite: σ_yy of element 1 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
1
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 0.0002
},
"reason": "as for the corner element"
},
{
"id": "finite_upper_top",
"quantity": "Finite: σ_yy of element 10 over that of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
10
]
},
"pointer": "/elements/0/stress/0/1",
"over": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 1.0,
"tolerance": {
"relative": 0.0002
},
"reason": "as for the corner element"
},
{
"id": "finite_lateral",
"quantity": "Finite: σ_xx of element 6 over its σ_yy (uniaxial stress: 0)",
"measure": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
6
]
},
"pointer": "/elements/0/stress/0/0",
"over": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
6
]
},
"pointer": "/elements/0/stress/0/1"
}
},
"value": 0.0,
"tolerance": {
"absolute": 0.0002
},
"reason": "a frictionless interface (and symmetry plane) leaves the blocks free to expand sideways; the non-matching interface must not hold them"
},
{
"id": "finite_pressure",
"quantity": "Finite: the contact pressure at the secondary node (0.5, 1) over −σ_yy of element 7",
"measure": {
"command": "solve.probe",
"params": {
"step": "Finite",
"at": [
[
0.5,
1.0,
0.0
]
]
},
"pointer": "/nodes/0/contact/0/pressure",
"over": {
"command": "solve.probe",
"params": {
"step": "Finite",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1",
"scale": -1.0
}
},
"value": 1.0,
"tolerance": {
"relative": 0.0002
},
"reason": "the mortar multipliers are the nodal values of the contact pressure, uniform and equal to the stress across the interface"
},
{
"id": "finite_force",
"quantity": "Finite: the integrated contact pressure over the reaction at the pushed top",
"measure": {
"from": "finite",
"pointer": "/contacts/0/normal_force",
"over": {
"command": "results.reactions",
"params": {
"step": "Finite",
"sets": [
"top"
]
},
"pointer": "/sets/0/force/1",
"scale": -1.0
}
},
"value": 1.0,
"tolerance": {
"relative": 0.0002
},
"reason": "the pressure carries the whole load across the interface (both are totals over the thickness)"
},
{
"id": "small_stress",
"quantity": "Small: σ_yy of element 7 (plane strain uniaxial stress E ε/(1 − ν²) = −10.989 for the 1% squeeze)",
"measure": {
"command": "solve.probe",
"params": {
"step": "Small",
"elements": [
7
]
},
"pointer": "/elements/0/stress/0/1"
},
"value": -10.989010989010989,
"tolerance": {
"relative": 0.0001
},
"reason": "the squeeze less the penetration the augmented Lagrangian leaves (up to 10⁻⁵ of the element thickness)"
}
]
}
@@ -0,0 +1,65 @@
{
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
"values": {
"finite_force": {
"band": 1.0000000000022103e-9,
"value": 1.0000000000022102
},
"finite_lateral": {
"band": 0.0002,
"value": -1.8586502985840345e-6
},
"finite_lower_base": {
"band": 1e-9,
"value": 0.9999968799408236
},
"finite_lower_corner": {
"band": 1.0000007080194146e-9,
"value": 1.0000007080194144
},
"finite_lower_far": {
"band": 1e-9,
"value": 0.9999913032274111
},
"finite_pressure": {
"band": 1e-9,
"value": 0.9999924972272739
},
"finite_upper_top": {
"band": 1e-9,
"value": 0.9999957165083484
},
"small_force": {
"band": 1e-9,
"value": 0.9999999999999859
},
"small_lateral": {
"band": 1e-6,
"value": -8.925527731170228e-15
},
"small_lower_base": {
"band": 1e-9,
"value": 0.9999999999999801
},
"small_lower_corner": {
"band": 1.0000000000000059e-9,
"value": 1.0000000000000058
},
"small_lower_far": {
"band": 1e-9,
"value": 0.9999999999999546
},
"small_pressure": {
"band": 1e-9,
"value": 0.9999999999999142
},
"small_stress": {
"band": 1.098901098901099e-8,
"value": -10.988443575433445
},
"small_upper_top": {
"band": 1e-9,
"value": 0.9999999999999762
}
}
}
@@ -0,0 +1,22 @@
[
{"command": "mesh.block", "params": {"body": "Lower", "kind": "quad4", "corners": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], "divisions": [3, 2], "bias": [2.0, 1.0], "edges": [{"corners": [1, 2], "name": "bottom"}, {"corners": [3, 4], "name": "top"}, {"corners": [4, 1], "name": "x0"}]}},
{"command": "mesh.block", "params": {"body": "Upper", "kind": "quad4", "corners": [[0, 1, 0], [1, 1, 0], [1, 2, 0], [0, 2, 0]], "divisions": [2, 2], "edges": [{"corners": [1, 2], "name": "bottom"}, {"corners": [3, 4], "name": "top"}, {"corners": [4, 1], "name": "x0"}], "merge": 0}},
{"command": "model.set.create", "params": {"name": "lower", "geometry": [{"body": "Lower", "face": "area"}]}},
{"command": "model.set.create", "params": {"name": "upper", "geometry": [{"body": "Upper", "face": "area"}]}},
{"command": "model.set.create", "params": {"name": "lower_top", "geometry": [{"body": "Lower", "edge": "top"}]}},
{"command": "model.set.create", "params": {"name": "upper_bottom", "geometry": [{"body": "Upper", "edge": "bottom"}]}},
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Lower", "edge": "x0"}, {"body": "Upper", "edge": "x0"}]}},
{"command": "model.set.create", "params": {"name": "base", "geometry": [{"body": "Lower", "edge": "bottom"}]}},
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Upper", "edge": "top"}]}},
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}}},
{"command": "model.section.create", "params": {"name": "lower", "set": "lower", "material": "Mat", "kind": {"type": "plane_strain", "thickness": 2.0}}},
{"command": "model.section.create", "params": {"name": "upper", "set": "upper", "material": "Mat", "kind": {"type": "plane_strain", "thickness": 2.0}}},
{"command": "interaction.contact.pair", "params": {"name": "interface", "main": "lower_top", "secondary": "upper_bottom"}},
{"command": "model.boundary_condition.create", "params": {"name": "x0", "type": "displacement", "target": {"set": "x0"}, "values": [0, null, null, null, null, null]}},
{"command": "model.boundary_condition.create", "params": {"name": "base", "type": "displacement", "target": {"set": "base"}, "values": [null, 0, null, null, null, null]}},
{"command": "model.boundary_condition.create", "params": {"name": "push", "type": "displacement", "target": {"set": "top"}, "values": [null, -0.02, null, null, null, null]}},
{"command": "model.step.create", "params": {"name": "Small", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false, "tolerances": {"force": 1e-10, "displacement": 1e-08, "energy": 1e-16}}, "boundary_conditions": ["x0", "base", "push"], "loads": []}},
{"command": "model.step.create", "params": {"name": "Finite", "procedure": {"type": "nonlinear_static"}, "boundary_conditions": ["x0", "base", "push"], "loads": []}},
{"command": "solve.nonlinear", "params": {"step": "Small"}, "id": "small"},
{"command": "solve.nonlinear", "params": {"step": "Finite"}, "id": "finite"}
]
@@ -0,0 +1,12 @@
# Hertz line contact in plane strain (implicit mortar contact of 2D edges)
The 2D form of `nl-hertz-cylinder`: the same steel cylinder of radius R = 10 mm on a steel block
(E = 210 GPa, ν = 0.3; mm, N, MPa), now a plane strain model of unit thickness meshed with quad4:
the cylinder's lower part a 2.5 mm wide block whose bottom edge is the circular arc (20 × 10,
graded towards the contact), on a 5 × 5 mm foundation (24 × 14), symmetric about x = 0. A
pressure on the indenter's top edge carries the load P = 2265.57 N/mm (per length of the whole
cylinder) for Hertz's half-width b = 0.5 mm and peak pressure p₀ = 2884.6 MPa. Soft springs hold
the indenter's top until the contact closes. One nonlinear static step without nonlinear
geometry: small sliding, edge-to-edge mortar contact of the round edge (secondary) on the flat
(main) by an augmented Lagrangian. The references and tolerances are the 3D case's, so the two
discretisations of one problem are checked against one closed form.
@@ -0,0 +1,125 @@
{
"title": "Hertz line contact: a cylinder pressed on a flat (implicit mortar contact of 2D edges, plane strain)",
"kind": "theory",
"source": "H. Hertz, Über die Berührung fester elastischer Körper, J. reine angew. Math. 92:156–171, 1882; K. L. Johnson, Contact Mechanics, Cambridge University Press 1985, §4.3 (two-dimensional contact of cylindrical bodies, eqs. 4.45–4.49): b = √(4PR/(πE*)), p₀ = 2P/(πb), with 1/E* = 2(1 − ν²)/E for two bodies of one material and R the cylinder's radius on a flat",
"quantities": [
{
"id": "peak_pressure",
"quantity": "the largest contact pressure p₀ (at x = 0)",
"measure": {
"from": "solve",
"pointer": "/contacts/0/max_pressure/value"
},
"value": 2884.6153846153843,
"tolerance": {
"relative": 0.02
},
"reason": "p₀ = 2P/(πb) for the load per length P = 2265.57 N/mm, b = 0.5 mm; the contact width spans seven elements of the graded mesh"
},
{
"id": "normal_force",
"quantity": "the force across the contact (half model, 1 mm thick)",
"measure": {
"from": "solve",
"pointer": "/contacts/0/normal_force"
},
"value": 1132.7858125924733,
"tolerance": {
"relative": 0.002
},
"reason": "the pressure carries the whole load; the soft springs that hold the indenter before contact take a millionth"
},
{
"id": "pressure_x0296",
"quantity": "the contact pressure at the secondary node at x = 0.29594",
"measure": {
"command": "solve.probe",
"params": {
"step": "Press",
"at": [
[
0.29594,
0.00438,
0.0
]
]
},
"pointer": "/nodes/0/contact/0/pressure"
},
"value": 2325.066800691437,
"tolerance": {
"relative": 0.05
},
"reason": "p₀ √(1 − x²/b²) at the node; the nodal pressure (the node's normal force over the area it carries) averages the distribution over the node's elements"
},
{
"id": "pressure_x0367",
"quantity": "the contact pressure at the secondary node at x = 0.36701",
"measure": {
"command": "solve.probe",
"params": {
"step": "Press",
"at": [
[
0.36701,
0.006737,
0.0
]
]
},
"pointer": "/nodes/0/contact/0/pressure"
},
"value": 1959.0164149439815,
"tolerance": {
"relative": 0.1
},
"reason": "as at node 7, nearer the edge, where the elements round off the square-root fall of the pressure to zero at b"
},
{
"id": "pressure_x0633",
"quantity": "the contact pressure at the secondary node at x = 0.63325 (beyond the half-width b = 0.5)",
"measure": {
"command": "solve.probe",
"params": {
"step": "Press",
"at": [
[
0.63325,
0.02007,
0.0
]
]
},
"pointer": "/nodes/0/contact/0/pressure"
},
"value": 0.0,
"tolerance": {
"absolute": 1.0
},
"reason": "outside the contact the surfaces are apart: no pressure (an element beyond b, since the edge element carries part of the last closed node's pressure)"
},
{
"id": "syy_depth",
"quantity": "the vertical stress σ_yy under the centre of contact at depth 0.47127",
"measure": {
"command": "solve.probe",
"params": {
"step": "Press",
"at": [
[
0.0,
-0.47127,
0.0
]
]
},
"pointer": "/nodes/0/stress/1"
},
"value": -2099.1580228637245,
"tolerance": {
"relative": 0.04
},
"reason": "σ_z = −p₀ / √(1 + z²/b²) on the axis of a half-plane (Johnson 1985, eq. 4.49); the foundation is ten half-widths wide and deep, not a half-plane, and the stress is the nodal average of the elements around the node"
}
]
}
@@ -0,0 +1,29 @@
{
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
"values": {
"normal_force": {
"band": 1.1330442926242355e-6,
"value": 1133.0442926242354
},
"peak_pressure": {
"band": 2.8919633998947767e-6,
"value": 2891.9633998947766
},
"pressure_x0296": {
"band": 2.325066800691437e-6,
"value": 2257.8514097926386
},
"pressure_x0367": {
"band": 2.078406343070884e-6,
"value": 2078.406343070884
},
"pressure_x0633": {
"band": 1.0,
"value": -0.0
},
"syy_depth": {
"band": 2.0991580228637247e-6,
"value": -2059.829517894975
}
}
}
@@ -0,0 +1,21 @@
[
{"command": "mesh.block", "params": {"body": "Indenter", "kind": "quad4", "corners": [[0, 0, 0], [2.5, 0.3175416344814579, 0], [2.5, 3.0, 0], [0, 3.0, 0]], "divisions": [20, 10], "bias": [8, 8], "edges": [{"corners": [1, 2], "arc": {"center": [0, 10.0, 0]}, "name": "round"}, {"corners": [3, 4], "name": "top"}, {"corners": [4, 1], "name": "sym"}]}},
{"command": "mesh.block", "params": {"body": "Foundation", "kind": "quad4", "corners": [[0, -5, 0], [5, -5, 0], [5, 0, 0], [0, 0, 0]], "divisions": [24, 14], "bias": [10, 0.1], "edges": [{"corners": [1, 2], "name": "base"}, {"corners": [3, 4], "name": "flat"}, {"corners": [4, 1], "name": "sym"}], "merge": 0}},
{"command": "model.set.create", "params": {"name": "indenter", "geometry": [{"body": "Indenter", "face": "area"}]}},
{"command": "model.set.create", "params": {"name": "foundation", "geometry": [{"body": "Foundation", "face": "area"}]}},
{"command": "model.set.create", "params": {"name": "round", "geometry": [{"body": "Indenter", "edge": "round"}]}},
{"command": "model.set.create", "params": {"name": "flat", "geometry": [{"body": "Foundation", "edge": "flat"}]}},
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Indenter", "edge": "top"}]}},
{"command": "model.set.create", "params": {"name": "base", "geometry": [{"body": "Foundation", "edge": "base"}]}},
{"command": "model.set.create", "params": {"name": "sym", "geometry": [{"body": "Indenter", "edge": "sym"}, {"body": "Foundation", "edge": "sym"}]}},
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 210000.0, "poisson": 0.3}}},
{"command": "model.section.create", "params": {"name": "indenter", "set": "indenter", "material": "Steel", "kind": {"type": "plane_strain", "thickness": 1.0}}},
{"command": "model.section.create", "params": {"name": "foundation", "set": "foundation", "material": "Steel", "kind": {"type": "plane_strain", "thickness": 1.0}}},
{"command": "interaction.contact.pair", "params": {"name": "hertz", "main": "flat", "secondary": "round"}},
{"command": "model.boundary_condition.create", "params": {"name": "sym", "type": "displacement", "target": {"set": "sym"}, "values": [0, null, null, null, null, null]}},
{"command": "model.boundary_condition.create", "params": {"name": "base", "type": "displacement", "target": {"set": "base"}, "values": [null, 0, null, null, null, null]}},
{"command": "model.spring.create", "params": {"name": "hold", "type": "spring", "first": {"set": "top"}, "direction": {"dof": 2}, "stiffness": 0.01}},
{"command": "model.load.create", "params": {"name": "press", "type": "pressure", "set": "top", "magnitude": 453.1143250369893}},
{"command": "model.step.create", "params": {"name": "Press", "procedure": {"type": "nonlinear_static", "nonlinear_geometry": false, "max_iterations": 60}, "boundary_conditions": ["sym", "base"], "loads": ["press"]}},
{"command": "solve.nonlinear", "params": {"step": "Press"}, "id": "solve"}
]