model: contact pairs, general contact and Coulomb friction, mapped by the deck writers

Contact definitions on face sets: a pair (each surface's nodes against the
other's faces), general contact on a surface (self contact) or on every
exterior face of the solids; Coulomb friction with static and kinetic
coefficients and an exponential decay, a penalty scale and the choice to
adjust or refuse initially penetrating nodes, validated with caps. Rigid
walls of explicit steps gain friction. The check wants solid faces on
contact surfaces; sets used by a contact cannot be deleted.

fem::contact: the closest point of a triangle (Ericson) or a bilinear
quadrilateral (projected Newton) to a point, and a bucket grid of face
boxes for the global search, deterministic and bounded.

Commands interaction.contact.pair, .general, .edit, .delete and .find
(the touching parts of the mesh, with face sets and pairs made of them);
contacts in the model browser. Abaqus writes *SURFACE INTERACTION with
*FRICTION (exponential decay), *CONTACT PAIR and general *CONTACT; LS-DYNA
*CONTACT_AUTOMATIC_SURFACE_TO_SURFACE and _SINGLE_SURFACE with FS, FD, DC;
Nastran refuses contact by name. The implicit solver refuses contact by
name (M7); the explicit solver refuses it until the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 12:49:45 -04:00
1 parent 6c2b2509e7
commit ecf947c090
23 files changed
+1494 -29

No files matched your search

+16 -6
View File
@@ -10,7 +10,7 @@ use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::{Error, Model, Result, check_name, context, finite, positive, require};
use crate::{Error, Friction, Model, Result, check_name, context, finite, positive, require};
/// Most steps in a model.
pub const MAX_STEPS: usize = 1000;
@@ -317,7 +317,7 @@ pub struct ExplicitDynamic {
/// the last one listed).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub initial_velocities: Vec<InitialVelocity>,
/// Fixed, frictionless rigid planes the nodes of sets cannot pass through.
/// Fixed rigid planes the nodes of sets cannot pass through, with Coulomb friction.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rigid_walls: Vec<RigidWall>,
/// Hourglass control of one-point hexahedra.
@@ -431,6 +431,7 @@ impl ExplicitDynamic {
if !(w.normal.iter().map(|c| c * c).sum::<f64>() > 0.0) {
return Err(Error::Invalid("a rigid wall's normal must not be zero".into()));
}
w.friction.validate().map_err(|e| Error::Invalid(format!("rigid wall on set `{}`: {e}", w.set)))?;
}
self.hourglass.validate()?;
self.bulk_viscosity.validate()
@@ -445,14 +446,16 @@ pub struct InitialVelocity {
pub velocity: [f64; 3],
}
/// A fixed, frictionless rigid plane through `point`. Its `normal` points to the side the nodes
/// of `set` stay on.
/// A fixed rigid plane through `point`. Its `normal` points to the side the nodes of `set` stay
/// on; nodes sliding on it feel `friction` (frictionless by default).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RigidWall {
pub set: String,
pub point: [f64; 3],
pub normal: [f64; 3],
#[serde(default, skip_serializing_if = "Friction::is_frictionless")]
pub friction: Friction,
}
/// How one-point hexahedra resist their hourglass (zero-energy) modes.
@@ -870,7 +873,7 @@ mod tests {
let json = serde_json::json!({
"type": "explicit_dynamic", "end_time": 1e-3, "history_nodes": "front",
"initial_velocities": [{"set": "front", "velocity": [-10, 0, 0]}],
"rigid_walls": [{"set": "front", "point": [0, 0, 0], "normal": [1, 0, 0]}],
"rigid_walls": [{"set": "front", "point": [0, 0, 0], "normal": [1, 0, 0], "friction": {"static": 0.2}}],
"hourglass": {"type": "stiffness"}
});
let p: Procedure = serde_json::from_value(json).unwrap();
@@ -895,7 +898,14 @@ mod tests {
ExplicitDynamic { max_increments: 0, ..base.clone() },
ExplicitDynamic { output_interval: Some(1e-9), ..base.clone() },
ExplicitDynamic { history_interval: Some(-1.0), ..base.clone() },
ExplicitDynamic { rigid_walls: vec![RigidWall { set: "front".into(), point: [0.0; 3], normal: [0.0; 3] }], ..base.clone() },
ExplicitDynamic {
rigid_walls: vec![RigidWall { set: "front".into(), point: [0.0; 3], normal: [0.0; 3], friction: Friction::NONE }],
..base.clone()
},
ExplicitDynamic {
rigid_walls: vec![RigidWall { set: "front".into(), point: [0.0; 3], normal: [1.0, 0.0, 0.0], friction: Friction::coulomb(f64::NAN) }],
..base.clone()
},
ExplicitDynamic {
initial_velocities: vec![InitialVelocity { set: "front".into(), velocity: [f64::INFINITY, 0.0, 0.0] }],
..base.clone()