engine: rubber-seal sample, a Mooney-Rivlin gasket ring squeezed 25 %

file.sample rubber-seal: a gasket ring of hybrid hex8 elements in four
mapped quarter blocks, squeezed 2.5 mm between plates bonded to it
(nonlinear static, Mooney-Rivlin rubber). Its test frees the plates and
matches the closed form for incompressible uniaxial compression within 3 %,
and bounds the bonded ring's extra stiffness. The plates are prescribed
displacements until implicit contact (#161) can press it into a groove.

Closes #163

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:55:22 -04:00
1 parent 581646bdac
commit cf8ed71e4f
5 files changed
+105 -1

No files matched your search

+1
View File
@@ -19,6 +19,7 @@ is committed and listed here with its exports.
| docs/media/og.svg, docs/media/og.png | FieldCraft contributors | Original social-preview image (1280 × 640): the application icon's cube layer (assets/app-icon/FieldCraft.icon/Assets/02-part.svg) beside the name and tagline set in Inter (OFL, crates/ui-egui/fonts) | MIT OR Apache-2.0 | | docs/media/og.svg, docs/media/og.png | FieldCraft contributors | Original social-preview image (1280 × 640): the application icon's cube layer (assets/app-icon/FieldCraft.icon/Assets/02-part.svg) beside the name and tagline set in Inter (OFL, crates/ui-egui/fonts) | MIT OR Apache-2.0 |
| docs/media/bracket-partition.png, docs/media/bracket-hex.png, docs/media/bracket-stress.png, docs/media/setup-symbols.png | FieldCraft contributors | Original: FieldCraft renders (headless MCP screenshots and a window capture) of corpus parts and the cantilever sample | MIT OR Apache-2.0 | | docs/media/bracket-partition.png, docs/media/bracket-hex.png, docs/media/bracket-stress.png, docs/media/setup-symbols.png | FieldCraft contributors | Original: FieldCraft renders (headless MCP screenshots and a window capture) of corpus parts and the cantilever sample | MIT OR Apache-2.0 |
| docs/media/examples/drop-test.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved drop-test sample (docs/examples.md) | MIT OR Apache-2.0 | | docs/media/examples/drop-test.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved drop-test sample (docs/examples.md) | MIT OR Apache-2.0 |
| docs/media/examples/rubber-seal.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved rubber-seal sample (docs/examples.md) | MIT OR Apache-2.0 |
| crates/decks/tests/golden/nastran/*.bdf | FieldCraft contributors | Written by our Nastran writer from our own test models (crates/decks/tests/nastran.rs) | MIT OR Apache-2.0 | | crates/decks/tests/golden/nastran/*.bdf | FieldCraft contributors | Written by our Nastran writer from our own test models (crates/decks/tests/nastran.rs) | MIT OR Apache-2.0 |
| assets/icons/assembly.instance.svg, assets/icons/assembly.instance.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/assembly.instance.json` (`cargo xtask icons`) | MIT OR Apache-2.0 | | assets/icons/assembly.instance.svg, assets/icons/assembly.instance.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/assembly.instance.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/deck.read.svg, assets/icons/deck.read.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.read.json` (`cargo xtask icons`) | MIT OR Apache-2.0 | | assets/icons/deck.read.svg, assets/icons/deck.read.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.read.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
+1 -1
View File
@@ -809,7 +809,7 @@ pub const RECOVERY_LIST: &[Field] = &[path("dir", "Folder", false).hint("the rec
pub const RECOVER: &[Field] = &[text("id", "Autosave").req(), path("dir", "Folder", false).hint("the recovery folder")]; pub const RECOVER: &[Field] = &[text("id", "Autosave").req(), path("dir", "Folder", false).hint("the recovery folder")];
pub const RECOVERY_DISCARD: &[Field] = &[text("id", "Autosave"), flag("all", "All"), path("dir", "Folder", false).hint("the recovery folder")]; pub const RECOVERY_DISCARD: &[Field] = &[text("id", "Autosave"), flag("all", "All"), path("dir", "Folder", false).hint("the recovery folder")];
pub const SAMPLE: &[Field] = pub const SAMPLE: &[Field] =
&[choice("name", "Sample", &[("cantilever", "Cantilever"), ("impact", "Impact"), ("parts", "Parts"), ("drop-test", "Drop test")]).req().def("\"cantilever\"")]; &[choice("name", "Sample", &[("cantilever", "Cantilever"), ("impact", "Impact"), ("parts", "Parts"), ("drop-test", "Drop test"), ("rubber-seal", "Rubber seal")]).req().def("\"cantilever\"")];
pub const COUNT: &[Field] = &[int("count", "Count", 1, 1_000_000).def("1")]; pub const COUNT: &[Field] = &[int("count", "Count", 1, 1_000_000).def("1")];
pub const DECK: &[Field] = &[ pub const DECK: &[Field] = &[
+83
View File
@@ -61,6 +61,13 @@ pub const SAMPLES: &[Sample] = &[
design: Some(UPRIGHT_BRACKET), design: Some(UPRIGHT_BRACKET),
script: |_| drop_test(), script: |_| drop_test(),
}, },
Sample {
name: "rubber-seal",
label: "Rubber Seal",
about: "A rubber gasket ring squeezed 25 % between bonded plates: nonlinear static, Mooney–Rivlin, large strain",
design: None,
script: |_| rubber_seal(),
},
]; ];
/// The sample called `name`. /// The sample called `name`.
@@ -288,6 +295,44 @@ pub fn drop_test() -> Value {
]) ])
} }
/// The rubber seal: inner and outer radius and height of the ring (mm), and how far it is squeezed.
pub const SEAL: [f64; 3] = [20.0, 30.0, 10.0];
pub const SEAL_SQUEEZE: f64 = 2.5;
/// A rubber gasket ring ([`SEAL`]: 20 mm inside radius, 30 mm outside, 10 mm high) of hybrid hex8
/// elements, four mapped quarter blocks with arc edges (body `Seal`, block faces `bottom` and
/// `top`), squeezed by [`SEAL_SQUEEZE`] (25 %) between plates bonded to it: `bottom` fixed, `top`
/// moved down with its in-plane motion held. Generic rubber: E = 6 MPa, ν = 0.49, Mooney–Rivlin
/// with C01 = 0.2 MPa (C10 = μ/2 − C01 = 0.81 MPa). Step `Squeeze` (nonlinear static).
pub fn rubber_seal() -> Value {
let [inner, outer, height] = SEAL;
let mut steps = vec![json!({"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}})];
for quarter in 0..4u8 {
let (a0, a1) = (f64::from(quarter) * std::f64::consts::FRAC_PI_2, f64::from(quarter + 1) * std::f64::consts::FRAC_PI_2);
let at = |r: f64, a: f64, z: f64| [r * a.cos(), r * a.sin(), z];
let corners =
[at(inner, a0, 0.0), at(outer, a0, 0.0), at(outer, a1, 0.0), at(inner, a1, 0.0), at(inner, a0, height), at(outer, a0, height), at(outer, a1, height), at(inner, a1, height)];
let arc = |a: u8, b: u8, z: f64| json!({"corners": [a, b], "arc": {"center": [0, 0, z]}});
steps.push(json!({"command": "mesh.block", "params": {
"body": "Seal", "kind": "hex8", "corners": corners, "divisions": [5, 10, 5],
"edges": [arc(1, 4, 0.0), arc(2, 3, 0.0), arc(5, 8, height), arc(6, 7, height)],
"faces": {"1": "bottom", "2": "top"}
}}));
}
steps.extend([
json!({"command": "model.set.create", "params": {"name": "seal", "geometry": [{"body": "Seal"}]}}),
json!({"command": "model.set.create", "params": {"name": "bottom", "geometry": [{"body": "Seal", "face": "bottom"}]}}),
json!({"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Seal", "face": "top"}]}}),
json!({"command": "model.material.create", "params": {"name": "Rubber", "elastic": {"type": "isotropic", "young": 6, "poisson": 0.49}, "density": 1.1e-9,
"hyperelastic": {"type": "mooney_rivlin", "c01": 0.2}}}),
json!({"command": "model.section.create", "params": {"name": "Seal", "set": "seal", "material": "Rubber", "kind": {"type": "solid"}, "formulation": {"hybrid": true}}}),
json!({"command": "model.boundary_condition.create", "params": {"name": "Base", "type": "fixed", "target": {"set": "bottom"}}}),
json!({"command": "model.boundary_condition.create", "params": {"name": "Press", "type": "displacement", "target": {"set": "top"}, "values": [0, 0, -SEAL_SQUEEZE, null, null, null]}}),
json!({"command": "model.step.create", "params": {"name": "Squeeze", "procedure": {"type": "nonlinear_static", "initial_increment": 0.1}, "boundary_conditions": ["Base", "Press"]}}),
]);
Value::Array(steps)
}
/// Six times the signed volume of the tetrahedron `p[0..4]` (positive when p3 lies on the side /// Six times the signed volume of the tetrahedron `p[0..4]` (positive when p3 lies on the side
/// p0 → p1 → p2 turns counter-clockwise around). /// p0 → p1 → p2 turns counter-clockwise around).
fn signed_volume(p: &[[f64; 3]]) -> f64 { fn signed_volume(p: &[[f64; 3]]) -> f64 {
@@ -365,6 +410,44 @@ mod tests {
s s
} }
/// The squeeze force of the ring, against the closed form for an incompressible Mooney–Rivlin
/// solid in uniaxial compression, P = 2 (λ − λ⁻²)(C10 + C01/λ) per unit of original area: the
/// same ring between frictionless plates (each face held only along z, the rigid motions
/// held at three nodes on the symmetry planes) compresses evenly and matches it within 3 %
/// (ν = 0.49 is nearly incompressible); bonded, the bulge needs more force (shape factor
/// ¼ (b − a)/h = 0.5 by the loaded over the free area), within 2.5 times.
#[test]
fn the_rubber_seal_squeezes_as_mooney_rivlin_rubber() {
let [inner, outer, height] = SEAL;
let area = std::f64::consts::PI * (outer * outer - inner * inner);
let lambda: f64 = 1.0 - SEAL_SQUEEZE / height;
let (c01, c10) = (0.2, 6.0 / (2.0 * 1.49) / 2.0 - 0.2);
let closed = 2.0 * (lambda - lambda.powi(-2)) * (c10 + c01 / lambda) * area;
let squeeze = |s: &mut Session| -> f64 {
s.execute("solve.nonlinear", &json!({})).unwrap();
let r = s.execute("results.reactions", &json!({"step": "Squeeze", "sets": ["top"]})).unwrap();
r["sets"][0]["force"][2].as_f64().unwrap()
};
let mut s = open("rubber-seal");
let bonded = squeeze(&mut s);
let node = |s: &Session, xyz: [f64; 3]| -> u32 {
*s.model.nodes().iter().find(|(_, n)| (0..3).all(|k| (n.xyz[k] - xyz[k]).abs() < 1e-9)).unwrap().0
};
let held = [("hold_y", [inner, 0.0, 0.0], [None, Some(0.0), None]), ("hold_x", [0.0, inner, 0.0], [Some(0.0), None, None]), ("hold_y2", [-inner, 0.0, 0.0], [None, Some(0.0), None])];
for (name, at, [ux, uy, _]) in held {
let n = node(&s, at);
s.execute("model.set.create", &json!({"name": name, "nodes": [n]})).unwrap();
s.execute("model.boundary_condition.create", &json!({"name": name, "type": "displacement", "target": {"set": name}, "values": [ux, uy, null, null, null, null]})).unwrap();
}
s.execute("model.boundary_condition.edit", &json!({"name": "Base", "type": "displacement", "target": {"set": "bottom"}, "values": [null, null, 0, null, null, null]})).unwrap();
s.execute("model.boundary_condition.edit", &json!({"name": "Press", "type": "displacement", "target": {"set": "top"}, "values": [null, null, -SEAL_SQUEEZE, null, null, null]})).unwrap();
s.execute("model.step.edit", &json!({"name": "Squeeze", "boundary_conditions": ["Base", "Press", "hold_y", "hold_x", "hold_y2"]})).unwrap();
let free = squeeze(&mut s);
assert!((free - closed).abs() < 0.03 * closed.abs(), "frictionless {free} N against {closed} N");
let stiffer = bonded / free;
assert!(stiffer > 1.05 && stiffer < 2.5, "bonded {bonded} N, frictionless {free} N");
}
#[test] #[test]
fn the_drop_test_opens_ready_to_solve() { fn the_drop_test_opens_ready_to_solve() {
let s = open("drop-test"); let s = open("drop-test");
+20
View File
@@ -26,3 +26,23 @@ turning into vibration with a balance under 0.01 %.
**Check:** the mass equals ρ V of the bracket (0.5 %); it meets the floor at √(2 g h) = **Check:** the mass equals ρ V of the bracket (0.5 %); it meets the floor at √(2 g h) =
6.26 m/s, with ½ m v² = 1 748 mJ of kinetic energy (1 %); energy is conserved (balance < 1 %); 6.26 m/s, with ½ m v² = 1 748 mJ of kinetic energy (1 %); energy is conserved (balance < 1 %);
by 1 ms the floor has taken about half its downward momentum. by 1 ms the floor has taken about half its downward momentum.
## Rubber seal (`rubber-seal`)
![The gasket ring squeezed 25 %, bulging outward, von Mises stress peaking at the bonded edges](media/examples/rubber-seal.png)
A rubber gasket ring (20 mm inside radius, 30 mm outside, 10 mm high) squeezed 2.5 mm (25 %)
between two plates bonded to it. Generic rubber, E = 6 MPa, ν = 0.49, Mooney–Rivlin with
C01 = 0.2 MPa; 1 000 hybrid (u–p) hex8 elements in four mapped quarter blocks. Nonlinear static
with large deformation: Newton–Raphson in nine increments, under a second.
**Look at:** the deformed shape at scale 1 — the free inner and outer walls bulge out, and
von Mises peaks (about 7 MPa) where the bulge meets the bonded plates. Results ▸ Increment steps
through the converged increments.
**Check:** the same ring between frictionless plates compresses evenly, and its force matches
the closed form for an incompressible Mooney–Rivlin solid in uniaxial compression,
P = 2 (λ − λ⁻²)(C10 + C01/λ) per original area: 3 430 N against 3 466 N (within 3 %). Bonded,
the bulge makes it stiffer (shape factor 0.5): 6 518 N, held between 1.05 and 2.5 times the
frictionless force. The groove and the plates are prescribed displacements today; with implicit
contact (#161) the ring will be pressed by contact against a groove.
Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB