forked from 1b-agent/fieldcraft-upstream
engine: heated-bracket sample, steady heat and thermal stress in steel
file.sample heated-bracket: the corpus filleted bracket in steel, tet10, its hole bores clamped, the upright's end held at 120 C and the rest cooled by still air; one coupled temperature-displacement step. Its test checks the temperatures against the heater and the air, then warms the bracket evenly, clamps one hole, and matches the free thermal expansion alpha dT times the distance from the hole along x. Closes #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
ecdff11e72
commit
f5a0a1289a
5 files changed
+107
-1
No files matched your search
@@ -20,6 +20,7 @@ is committed and listed here with its exports.
|
||||
| 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/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 |
|
||||
| docs/media/examples/heated-bracket.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved heated-bracket 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -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 RECOVERY_DISCARD: &[Field] = &[text("id", "Autosave"), flag("all", "All"), path("dir", "Folder", false).hint("the recovery folder")];
|
||||
pub const SAMPLE: &[Field] =
|
||||
&[choice("name", "Sample", &[("cantilever", "Cantilever"), ("impact", "Impact"), ("parts", "Parts"), ("drop-test", "Drop test"), ("rubber-seal", "Rubber seal"), ("flow-past", "Flow past a bracket")]).req().def("\"cantilever\"")];
|
||||
&[choice("name", "Sample", &[("cantilever", "Cantilever"), ("impact", "Impact"), ("parts", "Parts"), ("drop-test", "Drop test"), ("rubber-seal", "Rubber seal"), ("flow-past", "Flow past a bracket"), ("heated-bracket", "Heated bracket")]).req().def("\"cantilever\"")];
|
||||
pub const COUNT: &[Field] = &[int("count", "Count", 1, 1_000_000).def("1")];
|
||||
|
||||
pub const DECK: &[Field] = &[
|
||||
|
||||
@@ -75,6 +75,13 @@ pub const SAMPLES: &[Sample] = &[
|
||||
design: Some(UPRIGHT_BRACKET),
|
||||
script: |_| flow_past(),
|
||||
},
|
||||
Sample {
|
||||
name: "heated-bracket",
|
||||
label: "Heated Bracket",
|
||||
about: "A steel bracket clamped at its holes and heated at one end: steady heat conduction and the thermal stress",
|
||||
design: Some(FILLETED_BRACKET),
|
||||
script: |_| heated_bracket(),
|
||||
},
|
||||
];
|
||||
|
||||
/// The sample called `name`.
|
||||
@@ -367,6 +374,47 @@ pub fn flow_past() -> Value {
|
||||
])
|
||||
}
|
||||
|
||||
/// The corpus filleted bracket (`tests/corpus/filleted-bracket`): an 8 mm L-bracket, foot along
|
||||
/// x (60 mm) and upright along y (50 mm), extruded 40 mm along z, an R6 fillet, an 8 mm hole
|
||||
/// through each leg (`F4` through the foot at x = 40, `F5` through the upright at y = 32).
|
||||
pub const FILLETED_BRACKET: &str = include_str!("../../../tests/corpus/filleted-bracket/recipe.json");
|
||||
|
||||
/// The heated bracket: the heater's and the surrounding air's temperatures (°C, also the stress
|
||||
/// free temperature), and steel's expansion coefficient (1/K).
|
||||
pub const HEATER: f64 = 120.0;
|
||||
pub const AMBIENT: f64 = 20.0;
|
||||
pub const STEEL_EXPANSION: f64 = 1.2e-5;
|
||||
|
||||
/// [`FILLETED_BRACKET`] in steel (k = 50 W/(m K), α = 12 × 10⁻⁶/K, stress free at 20 °C), tet10
|
||||
/// elements of 4 mm: its upright's end (`F2:side:l5`, set `hot`) held at [`HEATER`], the rest of
|
||||
/// its outside (set `skin`) cooled by still air at [`AMBIENT`] (h = 10 W/(m² K)), both hole bores
|
||||
/// clamped (set `holes`). Step `Heat` (coupled temperature–displacement, steady): the
|
||||
/// temperature field and the stress of the expansion the clamps hold back. In mm, t, s, N, MPa:
|
||||
/// k in mW/(mm K), h in mW/(mm² K).
|
||||
pub fn heated_bracket() -> Value {
|
||||
let face = |f: &str| json!({"body": "Bracket", "face": f});
|
||||
let skin: Vec<Value> = ["F2:start", "F2:top", "F2:side:l1", "F2:side:l2", "F2:side:l3", "F2:side:l4", "F2:side:l6", "F3:blend:F2:side:l3|F2:side:l4"]
|
||||
.into_iter()
|
||||
.map(face)
|
||||
.collect();
|
||||
let holes: Vec<Value> = ["F4:bore:side:0", "F4:bore:side:1", "F5:bore:side:0", "F5:bore:side:1"].into_iter().map(face).collect();
|
||||
json!([
|
||||
{"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}},
|
||||
{"command": "mesh.tet.generate", "params": {"body": "Bracket", "size": 4, "order": "quadratic", "tolerance": 0.05}},
|
||||
{"command": "model.set.create", "params": {"name": "bracket", "geometry": [{"body": "Bracket"}]}},
|
||||
{"command": "model.set.create", "params": {"name": "holes", "geometry": holes}},
|
||||
{"command": "model.set.create", "params": {"name": "hot", "geometry": [face("F2:side:l5")]}},
|
||||
{"command": "model.set.create", "params": {"name": "skin", "geometry": skin}},
|
||||
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3}, "density": 7.85e-9,
|
||||
"thermal": {"conductivity": 50, "specific_heat": 4.6e8, "expansion": STEEL_EXPANSION, "reference_temperature": AMBIENT}}},
|
||||
{"command": "model.section.create", "params": {"name": "Bracket", "set": "bracket", "material": "Steel", "kind": {"type": "solid"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "Clamp", "type": "fixed", "target": {"set": "holes"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "Heater", "type": "temperature", "target": {"set": "hot"}, "value": HEATER}},
|
||||
{"command": "model.load.create", "params": {"name": "Air", "type": "convection", "set": "skin", "film": 0.01, "sink": AMBIENT}},
|
||||
{"command": "model.step.create", "params": {"name": "Heat", "procedure": {"type": "coupled_temperature_displacement"}, "boundary_conditions": ["Clamp", "Heater"], "loads": ["Air"]}},
|
||||
])
|
||||
}
|
||||
|
||||
/// 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).
|
||||
fn signed_volume(p: &[[f64; 3]]) -> f64 {
|
||||
@@ -505,6 +553,39 @@ mod tests {
|
||||
assert!(r["mass"]["relative_change"].as_f64().unwrap().abs() < 0.01, "{r}");
|
||||
}
|
||||
|
||||
/// The heated bracket's temperatures lie between the air's and the heater's, falling along
|
||||
/// the bracket away from the heater. Its thermal expansion: with the air at the heater's
|
||||
/// temperature the bracket warms evenly by ΔT, and held at the foot's hole alone (at x = 40)
|
||||
/// it grows freely, so a point moves α ΔT times its distance from the hole's axis along x:
|
||||
/// the upright's top by −0.0432 mm (within 2 %), the foot's end by +0.024 mm (within 6 %:
|
||||
/// the clamped bore cannot grow).
|
||||
#[test]
|
||||
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
|
||||
fn the_heated_bracket_warms_and_expands_as_steel() {
|
||||
let mut s = open("heated-bracket");
|
||||
let r = s.execute("solve.run", &json!({})).unwrap();
|
||||
let r = &r["steps"][0]["summary"];
|
||||
assert!((r["max_temperature"]["value"].as_f64().unwrap() - HEATER).abs() < 1e-9, "{r}");
|
||||
let probe = |s: &mut Session| s.execute("solve.probe", &json!({"step": "Heat", "at": [[4.0, 50.0, 20.0], [60.0, 4.0, 20.0]]})).unwrap();
|
||||
let p = probe(&mut s);
|
||||
let (upright, foot) = (p["nodes"][0]["temperature"].as_f64().unwrap(), p["nodes"][1]["temperature"].as_f64().unwrap());
|
||||
assert!(upright > foot && foot > AMBIENT && upright <= HEATER, "{upright} {foot}");
|
||||
let foot_hole = json!([{"body": "Bracket", "face": "F4:bore:side:0"}, {"body": "Bracket", "face": "F4:bore:side:1"}]);
|
||||
s.execute("model.set.create", &json!({"name": "foot_hole", "geometry": foot_hole})).unwrap();
|
||||
s.execute("model.boundary_condition.edit", &json!({"name": "Clamp", "type": "fixed", "target": {"set": "foot_hole"}})).unwrap();
|
||||
s.execute("model.load.edit", &json!({"name": "Air", "type": "convection", "set": "skin", "film": 0.01, "sink": HEATER})).unwrap();
|
||||
s.execute("solve.static", &json!({})).unwrap();
|
||||
let p = probe(&mut s);
|
||||
let grow = STEEL_EXPANSION * (HEATER - AMBIENT);
|
||||
// The bore itself cannot grow, which holds back the foot's end, 16 mm from its edge, a
|
||||
// little more than the upright's top, 32 mm away.
|
||||
for (i, tolerance) in [(0, 0.02), (1, 0.06)] {
|
||||
let node = &p["nodes"][i];
|
||||
let (ux, expected) = (node["displacement"][0].as_f64().unwrap(), grow * (node["xyz"][0].as_f64().unwrap() - 40.0));
|
||||
assert!((ux - expected).abs() < tolerance * expected.abs(), "{node}: {ux} against {expected}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_drop_test_opens_ready_to_solve() {
|
||||
let s = open("drop-test");
|
||||
|
||||
@@ -67,3 +67,27 @@ Reynolds number and rises as it falls); the lattice's mass changes by less than
|
||||
|
||||
There is no picture yet: the viewport does not draw flow results (velocity slices and
|
||||
streamlines are planned in M12), so this example is numbers only for now.
|
||||
|
||||
## Heated bracket (`heated-bracket`)
|
||||
|
||||

|
||||
|
||||
The corpus filleted bracket (`tests/corpus/filleted-bracket`: an 8 mm L-bracket, R6 fillet, a
|
||||
hole through each leg) in steel, both hole bores clamped. The end of the upright is held at
|
||||
120 °C by a heater; the rest of its outside loses heat to still air at 20 °C
|
||||
(h = 10 W/(m² K)); steel conducts with k = 50 W/(m K), expands with α = 12 × 10⁻⁶/K and is
|
||||
stress free at 20 °C. One coupled temperature–displacement step (steady) on 23 641 tet10
|
||||
elements: about 8 s to open (meshing) and 13 s to solve.
|
||||
|
||||
**Look at:** Contours ▸ Temperature: the upright stays within a few degrees of the heater and
|
||||
the foot falls to 95 °C, a steady balance between conduction and the air. Contours ▸ Von Mises
|
||||
shows the stress of the growth the clamps hold back: tens of MPa through the legs, peaking at the
|
||||
edges of the clamped bores, where a rigid clamp is a stress singularity (about 1 GPa there, a
|
||||
figure that grows as the mesh is refined: read the stress a little away from the bore).
|
||||
|
||||
**Check:** the temperatures lie between the air's and the heater's and fall away from the heater.
|
||||
For the expansion, the test puts the air at the heater's temperature, so the bracket warms
|
||||
evenly by ΔT = 100 K, and clamps the foot's hole alone: the bracket then grows freely about it,
|
||||
and a point moves α ΔT times its distance from the hole's axis. The upright's top moves −0.0432
|
||||
mm along x (within 2 %); the foot's end, 16 mm from the clamped bore, which cannot grow, moves
|
||||
0.0228 mm against 0.024 mm (within 6 %).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 350 KiB |
Reference in new issue
Block a user