engine: the I-beam cantilever sample (ibeam, #143)

The README video's model as a sample: the corpus cantilever-i-beam design opened as geometry,
swept all-hex, clamped at F2:start, 1 kN on F2:top through a kinematic coupling; static step
Load. It has a Start-card thumbnail and a gallery capture; a test checks its tip deflection
against beam theory with shear.

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-15 11:07:46 -04:00
1 parent 0625125ed7
commit 4711f9474a
7 files changed
+68 -1

No files matched your search

+54 -1
View File
@@ -11,7 +11,7 @@
//! explicit dynamic step.
//!
//! The examples gallery (docs/examples.md), each ready to solve with `solve.run`:
//! [`drop_test`], [`rubber_seal`], [`flow_past`], [`heated_bracket`], [`bracket_hex`] and [`crank_arm`]. Their
//! [`ibeam`], [`drop_test`], [`rubber_seal`], [`flow_past`], [`heated_bracket`], [`bracket_hex`] and [`crank_arm`]. Their
//! geometry is a SolidCraft script run when the sample opens ([`Sample::design`]): a corpus
//! recipe (`tests/corpus`) or [`UPRIGHT_BRACKET`].
@@ -60,6 +60,14 @@ pub const SAMPLES: &[Sample] = &[
prepare: None,
script: |_| impact(),
},
Sample {
name: "ibeam",
label: "I-Beam Cantilever",
about: "A 200 mm steel I-beam swept all-hex, clamped at one end with 1 kN on the other through a coupling: static, ready to solve",
design: Some(CANTILEVER_I_BEAM),
prepare: None,
script: |_| ibeam(),
},
Sample {
name: "drop-test",
label: "Drop Test",
@@ -290,6 +298,36 @@ pub fn impact() -> Value {
Value::Array(steps)
}
/// The 200 mm I-beam lying along x, web upright (`tests/corpus/cantilever-i-beam`): 40 × 40 mm,
/// 3 mm flanges, a 2 mm web. Body `Beam`; its end faces are `F2:start` (x = 0) and `F2:top`
/// (x = 200).
pub const CANTILEVER_I_BEAM: &str = include_str!("../../../tests/corpus/cantilever-i-beam/recipe.json");
/// The I-beam's tip load (N).
pub const IBEAM_LOAD: f64 = 1000.0;
/// [`CANTILEVER_I_BEAM`] swept all-hex (hex8 of 4 mm, incompatible modes) in steel, clamped at
/// x = 0 (set `root`), [`IBEAM_LOAD`] down (−z) on a reference point on the beam's axis at the
/// free end, kinematically coupled to the end face (set `tip`) so the section stays plane. Step
/// `Load` (linear static). The README demo's model (tools/demo-video).
pub fn ibeam() -> Value {
let face = |f: &str| json!([{"body": "Beam", "face": f}]);
json!([
{"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}},
{"command": "mesh.hex.sweep", "params": {"body": "Beam", "size": 4}},
{"command": "model.set.create", "params": {"name": "beam", "geometry": [{"body": "Beam"}]}},
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3}, "density": 7.85e-9}},
{"command": "model.section.create", "params": {"name": "Beam", "set": "beam", "material": "Steel", "kind": {"type": "solid"}, "formulation": {"incompatible_modes": true}}},
{"command": "model.set.create", "params": {"name": "root", "geometry": face("F2:start")}},
{"command": "model.boundary_condition.create", "params": {"name": "Clamp", "type": "fixed", "target": {"set": "root"}}},
{"command": "model.set.create", "params": {"name": "tip", "geometry": face("F2:top")}},
{"command": "model.reference_point.create", "params": {"name": "End", "xyz": [200, 0, 20]}},
{"command": "model.coupling.create", "params": {"name": "EndPlate", "reference_point": "End", "set": "tip", "kind": "kinematic"}},
{"command": "model.load.create", "params": {"name": "Push", "type": "force", "target": {"reference_point": "End"}, "vector": [0, 0, -IBEAM_LOAD]}},
{"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["Clamp"], "loads": ["Push"]}},
])
}
/// An L-bracket standing on its foot, Z up, as a SolidCraft script: a 60 × 40 mm foot and a 50 mm
/// upright, both 8 mm thick, with an R6 fillet inside the bend (the corpus `filleted-bracket`'s
/// profile without its holes, sketched on XZ so it sweeps along y). Body `Bracket`; its faces
@@ -646,6 +684,21 @@ mod tests {
assert_eq!(f["modes"].as_array().map(Vec::len), Some(6));
}
/// The I-beam opens swept all-hex and ready; solved, its tip deflection is the beam theory's
/// bending plus shear (P L³ / 3 E I + P L / G A_web: 0.149 + 0.035 mm) within 5 %.
#[test]
fn the_ibeam_bends_as_beam_theory() {
let mut s = open("ibeam");
assert!(s.model.elements().values().all(|e| e.kind == fieldcraft_model::ElementKind::Hex8));
s.execute("solve.static", &json!({})).unwrap();
let p = s.execute("solve.probe", &json!({"step": "Load", "at": [[200.0, 0.0, 20.0]]})).unwrap();
let tip = -p["nodes"][0]["displacement"][2].as_f64().unwrap();
let (e, g) = (210000.0, 210000.0 / 2.6);
let i = (40.0 * 40.0f64.powi(3) - 38.0 * 34.0f64.powi(3)) / 12.0;
let theory = IBEAM_LOAD * 200.0f64.powi(3) / (3.0 * e * i) + IBEAM_LOAD * 200.0 / (g * 2.0 * 37.0);
assert!((tip - theory).abs() < 0.05 * theory, "{tip} against {theory}");
}
#[test]
fn the_impact_runs_and_keeps_frames_with_stresses() {
let mut s = Session::default();