engine: friction output of implicit contact

solve.nonlinear counts the slipping nodes, solve.probe gives a secondary node's shear traction
and slip, and the results display draws contact_shear and contact_slip, contact_status coding
stick and slip.

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 15:14:14 -04:00
1 parent 6ca3233aaa
commit 32c5c1b307
7 files changed
+78 -19

No files matched your search

+1 -1
View File
@@ -41,7 +41,7 @@ pub const PAIR: CommandSpec = CommandSpec {
params: concat!(
r#"{"name": "impact", "main": "plate" (a set of element faces), "secondary": "ball" (another), "#,
r#""friction": {"static": 0.3, "kinetic": 0.2, "decay": 10} (Coulomb, μ = μk + (μs − μk) e^(−decay × slip rate); default frictionless), "penalty_scale": 1 (0 < scale ≤ 1: a factor on the penalty: explicit, the stiffest that leaves the time increment alone; nonlinear static, the augmented Lagrangian's), "initial_penetration": "adjust" (nodes that start inside: explicit moves them onto the surface, strain-free; nonlinear static takes the overlap out of their gap; both report them) | "refuse" (refuse the analysis, naming the first)"#,
r#"} — contact between two surfaces: in explicit dynamics each one's nodes are kept out of the other's faces; in nonlinear static steps `secondary` is the mortar (non-mortar) side whose pressure field is enforced against `main` by an augmented Lagrangian, frictionless so far (fails if the name is taken)"#
r#"} — contact between two surfaces: in explicit dynamics each one's nodes are kept out of the other's faces; in nonlinear static steps `secondary` is the mortar (non-mortar) side whose pressure field is enforced against `main` by an augmented Lagrangian, with Coulomb friction by a return map (static coefficient while a node sticks, kinetic once it slips) (fails if the name is taken)"#
),
form: Some(super::forms::CONTACT_PAIR),
kind: CommandKind::Model,
+2
View File
@@ -883,6 +883,8 @@ pub const CONTOUR: &[Field] = &[
("contact_pressure", "Contact pressure"),
("contact_gap", "Contact gap"),
("contact_status", "Contact status"),
("contact_shear", "Contact shear"),
("contact_slip", "Contact slip"),
("none", "None"),
],
)
+1 -1
View File
@@ -20,7 +20,7 @@ pub const DEFORMED: CommandSpec = CommandSpec {
pub const CONTOUR: CommandSpec = CommandSpec {
id: "results.contour",
label: "Contours",
params: r#"{"step": "Load", "field": "von_mises", "bands": 12} — contours of a nodal field: displacement, displacement_x/y/z, von_mises, max/mid/min_principal, stress_xx/yy/zz/xy/xz/yz (stresses for static steps), contact_pressure, contact_gap, contact_status (0 open, 1 closed; nonlinear static steps with contact, at the contacts' secondary surfaces); "none" turns them off; 2 to 24 bands"#,
params: r#"{"step": "Load", "field": "von_mises", "bands": 12} — contours of a nodal field: displacement, displacement_x/y/z, von_mises, max/mid/min_principal, stress_xx/yy/zz/xy/xz/yz (stresses for static steps), contact_pressure, contact_gap, contact_status (0 open, 1 closed or stick, 2 slip), contact_shear, contact_slip (friction; nonlinear static steps with contact, at the contacts' secondary surfaces); "none" turns them off; 2 to 24 bands"#,
form: Some(super::forms::CONTOUR),
kind: CommandKind::Session,
run: contour,
+35 -2
View File
@@ -36,7 +36,7 @@ pub const STATIC: CommandSpec = CommandSpec {
pub const NONLINEAR: CommandSpec = CommandSpec {
id: "solve.nonlinear",
label: "Solve Nonlinear Static Step",
params: r#"{"step": "Inflate", "max_dofs": 6000000, "max_factor_entries": 400000000, "max_newton_iterations": 100000, "threads": 8} — solves a nonlinear static step (the only one when `step` is left out) in load increments with Newton–Raphson iterations and keeps the result in the session: the final displacements, reactions, Cauchy stresses and logarithmic strains (small strains without nonlinear geometry), the equivalent plastic strain of plastic materials (the largest in the summary), every increment's load factor, iterations and residuals (force, displacement and energy, relative to their tolerances' scales), and a displacement frame per converged increment, and with contact pairs (interaction.contact.pair: mortar contact by augmented Lagrangian, frictionless) each contact's normal force, largest pressure, closed nodes, deepest penetration and augmentations (solve.probe gives each secondary node's pressure, gap and status); the caps may only be lowered (`max_newton_iterations` over the whole step); `threads` as for solve.static"#,
params: r#"{"step": "Inflate", "max_dofs": 6000000, "max_factor_entries": 400000000, "max_newton_iterations": 100000, "threads": 8} — solves a nonlinear static step (the only one when `step` is left out) in load increments with Newton–Raphson iterations and keeps the result in the session: the final displacements, reactions, Cauchy stresses and logarithmic strains (small strains without nonlinear geometry), the equivalent plastic strain of plastic materials (the largest in the summary), every increment's load factor, iterations and residuals (force, displacement and energy, relative to their tolerances' scales), and a displacement frame per converged increment, and with contact pairs (interaction.contact.pair: mortar contact by augmented Lagrangian, Coulomb friction by a return map) each contact's force, normal force, largest pressure, closed and slipping nodes, deepest penetration and augmentations (solve.probe gives each secondary node's pressure, gap and status, and with friction its shear traction and slip); the caps may only be lowered (`max_newton_iterations` over the whole step); `threads` as for solve.static"#,
form: Some(super::forms::SOLVE_NONLINEAR),
kind: CommandKind::Model,
run: run_nonlinear,
@@ -245,6 +245,7 @@ fn run_nonlinear(s: &mut Session, p: &Value) -> Result<Value> {
"force": c.force,
"max_pressure": largest(c.nodes.iter().copied().zip(c.pressure.iter().copied())),
"closed_nodes": c.closed(),
"slipping_nodes": c.status.iter().filter(|s| **s == fieldcraft_solver::contact::Status::Slip).count(),
"penetration": c.penetration,
"augmentations": c.augmentations,
})).collect::<Vec<_>>(),
@@ -635,7 +636,12 @@ fn probe_implicit(result: &ImplicitResult, step: &str, nodes: &[u32], p: &Value)
.iter()
.filter_map(|c| {
let k = c.nodes.binary_search(&l).ok()?;
Some(json!({"contact": c.name, "pressure": c.pressure.get(k), "gap": c.gap.get(k), "status": c.status.get(k)}))
let mut entry = json!({"contact": c.name, "pressure": c.pressure.get(k), "gap": c.gap.get(k), "status": c.status.get(k)});
if let (Some(shear), Some(slip)) = (c.shear.get(k), c.slip.get(k)) {
entry["shear"] = json!(shear);
entry["slip"] = json!(slip);
}
Some(entry)
})
.collect();
if !contact.is_empty() {
@@ -1588,4 +1594,31 @@ pub(crate) mod tests {
let e = s.execute("solve.static", &json!({"step": "Linear"})).unwrap_err().to_string();
assert!(e.contains("interface"), "{e}");
}
#[test]
fn solve_and_probe_a_frictional_contact() {
let mut s = Session::default();
pressed_blocks(&mut s);
s.execute("interaction.contact.edit", &json!({"name": "interface", "surfaces": {"type": "pair", "main": "lower_top", "secondary": "upper_bottom"}, "friction": {"static": 0.2}})).unwrap();
s.execute(
"model.boundary_condition.edit",
&json!({"name": "push", "type": "displacement", "target": {"set": "top"}, "values": [0.05, 0, -0.02, null, null, null]}),
)
.unwrap();
let r = s.execute("solve.nonlinear", &json!({"step": "Squeeze"})).unwrap();
let c = &r["contacts"][0];
assert_eq!(c["slipping_nodes"], 16, "{r}");
// Slipping, the interface carries μ N sideways.
let (n, t) = (c["force"][2].as_f64().unwrap(), c["force"][0].as_f64().unwrap());
assert!((t + 0.2 * n).abs() < 1e-3 * n, "{r}");
let third = 1.0 / 3.0;
let probe = s.execute("solve.probe", &json!({"step": "Squeeze", "at": [[third, third, 1.0]]})).unwrap();
let contact = &probe["nodes"][0]["contact"][0];
assert_eq!(contact["status"], "slip", "{probe}");
assert!(contact["shear"][0].as_f64().unwrap() < 0.0 && contact["slip"][0].as_f64().unwrap() > 0.0, "{probe}");
let v = s.execute("results.contour", &json!({"step": "Squeeze", "field": "contact_status"})).unwrap();
assert_eq!(v["contour"]["range"]["max"]["value"], 2.0, "{v}");
let v = s.execute("results.contour", &json!({"step": "Squeeze", "field": "contact_shear"})).unwrap();
assert!(v["contour"]["range"]["max"]["value"].as_f64().unwrap() > 1.0, "{v}");
}
}