forked from 1b-agent/fieldcraft-upstream
engine: crank-arm sample, a bicycle crank swept all-hex and solved
file.sample crank-arm: a 170 mm 6061-T6 crank built in SolidCraft (crates/engine/samples/crank-arm.json), swept along z by mesh.hex.generate as 7274 hex20 elements of 3 mm (scaled Jacobian min 0.17). The hex variant drops the pocket floor fillet and boss chamfers, and its root arcs meet the bosses 20 degrees off tangent so the arm's top face has no zero-angle cusp. The square bore is fixed and 1500 N acts on the pedal hole; the faces are picked by their surfaces, not their names. Its heavy test matches the root bending stress to M/Z = 62.6 MPa within 3 % (0.3 % in practice). 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
98d263ae54
commit
6bd52c0b29
7 files changed
+154
-3
No files matched your search
@@ -22,6 +22,7 @@ is committed and listed here with its exports.
|
||||
| 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 |
|
||||
| docs/media/examples/bracket-hex.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved bracket-hex sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||
| docs/media/examples/crank-arm.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved crank-arm 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 |
|
||||
|
||||
@@ -102,8 +102,8 @@ against a closed form or a reference solution ([docs/examples.md](docs/examples.
|
||||
|---|---|---|
|
||||
|  |  |  |
|
||||
| **Drop test** (`drop-test`): an aluminium bracket dropped 2 m onto a tilted floor; explicit dynamics, the strike's stress waves | **Rubber seal** (`rubber-seal`): a Mooney–Rivlin gasket ring squeezed 25 %; nonlinear static, large strain | **Heated bracket** (`heated-bracket`): a clamped steel bracket heated at one end; steady heat and thermal stress |
|
||||
|  | *No picture yet: the viewport does not draw flow results (M12).* | |
|
||||
| **Hex-meshed bracket** (`bracket-hex`): the bracket split into 30 cells, meshed all-hex (hex20), von Mises | **Flow past a bracket** (`flow-past`): air past an L-bracket on the floor; lattice Boltzmann flow, the drag and the force on each face | |
|
||||
|  | *No picture yet: the viewport does not draw flow results (M12).* |  |
|
||||
| **Hex-meshed bracket** (`bracket-hex`): the bracket split into 30 cells, meshed all-hex (hex20), von Mises | **Flow past a bracket** (`flow-past`): air past an L-bracket on the floor; lattice Boltzmann flow, the drag and the force on each face | **Bicycle crank arm** (`crank-arm`): an aluminium crank swept all-hex (hex20), a rider's weight on the pedal; root bending within 0.3 % of M/Z |
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{"commands": [
|
||||
{"command": "parameters.change", "params": {"name": "arm_thickness", "expression": "16 mm"}},
|
||||
{"command": "sketch.create", "params": {"plane": "XY", "name": "ArmProfile"}},
|
||||
{"command": "sketch.line", "params": {"points": [[15.471735240951828, -18.362928548901984], [160.67227699330076, -12.384082712040556]], "ids": ["lo"]}},
|
||||
{"command": "sketch.arc.center_point", "params": {"center": [160.91912663845798, -18.3790026658586], "start": [161.58293237729853, -12.415835559271503], "end": [160.67227699330076, -12.384082712040556]}},
|
||||
{"command": "sketch.arc.center_point", "params": {"center": [170.0, 0], "start": [161.58293237729853, -12.415835559271503], "end": [161.58293237729853, 12.415835559271503]}},
|
||||
{"command": "sketch.arc.center_point", "params": {"center": [160.91912663845798, 18.3790026658586], "start": [160.67227699330076, 12.384082712040556], "end": [161.58293237729853, 12.415835559271503]}},
|
||||
{"command": "sketch.line", "params": {"points": [[160.67227699330076, 12.384082712040556], [15.471735240951828, 18.362928548901984]], "ids": ["hi"]}},
|
||||
{"command": "sketch.arc.center_point", "params": {"center": [15.883151316213851, 28.354461805265395], "start": [13.455063072993756, 18.65372021077189], "end": [15.471735240951828, 18.362928548901984]}},
|
||||
{"command": "sketch.arc.center_point", "params": {"center": [0, 0], "start": [13.455063072993756, 18.65372021077189], "end": [13.455063072993756, -18.65372021077189]}},
|
||||
{"command": "sketch.arc.center_point", "params": {"center": [15.883151316213851, -28.354461805265395], "start": [15.471735240951828, -18.362928548901984], "end": [13.455063072993756, -18.65372021077189]}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.extrude", "params": {"distance": "arm_thickness", "body_name": "CrankArm", "name": "Arm"}},
|
||||
{"command": "solid.cylinder", "params": {"diameter": 46.0, "height": 22, "base": [0, 0, 0], "operation": "join", "targets": ["CrankArm"], "name": "SpindleBoss"}},
|
||||
{"command": "solid.cylinder", "params": {"diameter": 30.0, "height": 20, "base": [170.0, 0, 0], "operation": "join", "targets": ["CrankArm"], "name": "PedalBoss"}},
|
||||
{"command": "sketch.create", "params": {"plane": "XY", "name": "BackPocket"}},
|
||||
{"command": "sketch.rectangle.two_point", "params": {"p0": [48, -7], "p1": [130, 7]}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.extrude", "params": {"distance": 8, "operation": "cut", "targets": ["CrankArm"], "name": "Pocket"}},
|
||||
{"command": "sketch.create", "params": {"plane": "XY", "name": "SpindleSquare"}},
|
||||
{"command": "sketch.rectangle.center", "params": {"center": [0, 0], "corner": [6.35, 6.35]}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.extrude", "params": {"through_all": true, "operation": "cut", "targets": ["CrankArm"], "name": "SpindleTaper"}},
|
||||
{"command": "solid.fillet", "params": {"edges": [[6.35, 6.35, 10], [-6.35, 6.35, 10], [-6.35, -6.35, 10], [6.35, -6.35, 10]], "radius": 1.5, "name": "SquareCornerFillet"}},
|
||||
{"command": "solid.hole", "params": {"position": [170.0, 0, 20], "diameter": 14, "name": "PedalHole"}},
|
||||
{"command": "solid.fillet", "params": {"edges": [[48, -7, 4], [48, 7, 4], [130, -7, 4], [130, 7, 4]], "radius": 5, "name": "PocketCornerFillet"}}
|
||||
]}
|
||||
@@ -820,6 +820,7 @@ pub const SAMPLE: &[Field] = &[choice(
|
||||
("flow-past", "Flow past a bracket"),
|
||||
("heated-bracket", "Heated bracket"),
|
||||
("bracket-hex", "Hex-meshed bracket"),
|
||||
("crank-arm", "Bicycle crank arm"),
|
||||
],
|
||||
)
|
||||
.req()
|
||||
|
||||
@@ -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`] and [`bracket_hex`]. Their
|
||||
//! [`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`].
|
||||
|
||||
@@ -89,6 +89,13 @@ pub const SAMPLES: &[Sample] = &[
|
||||
design: Some(PARTITIONED_BRACKET),
|
||||
script: bracket_hex,
|
||||
},
|
||||
Sample {
|
||||
name: "crank-arm",
|
||||
label: "Bicycle Crank Arm",
|
||||
about: "An aluminium bicycle crank swept all-hex (hex20): square bore fixed, a rider's weight on the pedal, von Mises",
|
||||
design: Some(CRANK_ARM),
|
||||
script: crank_arm,
|
||||
},
|
||||
];
|
||||
|
||||
/// The sample called `name`.
|
||||
@@ -458,6 +465,72 @@ pub fn bracket_hex(bodies: &[Shape]) -> Value {
|
||||
])
|
||||
}
|
||||
|
||||
/// The bicycle crank arm (`crates/engine/samples/crank-arm.json`): a 170 mm aluminium arm, 16 mm
|
||||
/// thick and tapering from 38 to 24 mm, joined by tangent root arcs to a Ø46 × 22 spindle boss
|
||||
/// with a 12.7 mm square bore (R1.5 corners) and a Ø30 × 20 pedal boss with a Ø14 hole, and an
|
||||
/// 82 × 14 × 8 pocket in its back (R5 corners). Made to be swept all-hex by `mesh.hex.generate`
|
||||
/// as a stack of extrusions along z: no pocket floor fillet or boss chamfers, and the root arcs
|
||||
/// meet the bosses about 20° off tangent (tangent to a circle 0.5 mm inside each boss), since a
|
||||
/// tangent meeting leaves a zero-angle cusp on the arm's top face.
|
||||
pub const CRANK_ARM: &str = include_str!("../samples/crank-arm.json");
|
||||
|
||||
/// The crank arm's pedal load (N): a standing rider's weight on one pedal.
|
||||
pub const PEDAL_LOAD: f64 = 1500.0;
|
||||
|
||||
/// [`CRANK_ARM`] swept all-hex (hex20 of 3 mm), in 6061-T6 aluminium: the square bore's faces
|
||||
/// and corner fillets (set `spindle`) fixed, [`PEDAL_LOAD`] on the pedal hole (set `pedal`), in
|
||||
/// the crank's plane and across the arm (−y). Step `Pedal` (linear static). The faces are picked
|
||||
/// by their surfaces, not their names: the square bore's are the vertical planes 6.35 mm from the
|
||||
/// spindle axis and the R1.5 cylinders beside it, the pedal hole is the Ø14 cylinder on the
|
||||
/// pedal axis.
|
||||
pub fn crank_arm(bodies: &[Shape]) -> Value {
|
||||
let [spindle, pedal] = crank_faces(bodies);
|
||||
json!([
|
||||
{"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}},
|
||||
{"command": "mesh.hex.generate", "params": {"size": 3, "order": 2, "method": "extrude"}},
|
||||
{"command": "model.set.create", "params": {"name": "crank", "geometry": bodies.iter().map(|b| json!({"body": b.name})).collect::<Vec<_>>()}},
|
||||
{"command": "model.set.create", "params": {"name": "spindle", "geometry": spindle}},
|
||||
{"command": "model.set.create", "params": {"name": "pedal", "geometry": pedal}},
|
||||
{"command": "model.material.create", "params": {"name": "Al6061-T6", "elastic": {"type": "isotropic", "young": 68900, "poisson": 0.33}, "density": 2.7e-9}},
|
||||
{"command": "model.section.create", "params": {"name": "Crank", "set": "crank", "material": "Al6061-T6", "kind": {"type": "solid"}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "Spindle", "type": "fixed", "target": {"set": "spindle"}}},
|
||||
{"command": "model.load.create", "params": {"name": "Rider", "type": "force", "target": {"set": "pedal"}, "vector": [0, -PEDAL_LOAD, 0]}},
|
||||
{"command": "model.step.create", "params": {"name": "Pedal", "procedure": {"type": "static"}, "boundary_conditions": ["Spindle"], "loads": ["Rider"]}},
|
||||
])
|
||||
}
|
||||
|
||||
/// The crank's square-bore faces and pedal-hole faces as set members, found by their surfaces.
|
||||
fn crank_faces(bodies: &[Shape]) -> [Vec<Value>; 2] {
|
||||
use fieldcraft_geometry::FaceSurface;
|
||||
let vertical = |axis: [f64; 3]| axis[2].abs() > 1.0 - 1e-6;
|
||||
let (mut spindle, mut pedal) = (Vec::new(), Vec::new());
|
||||
for body in bodies {
|
||||
let surfaces = body.face_surfaces().unwrap_or_default();
|
||||
for (face, surface) in body.faces().iter().zip(surfaces) {
|
||||
let member = json!({"body": body.name, "face": face.name});
|
||||
match surface {
|
||||
Some(FaceSurface::Plane { normal, point })
|
||||
if normal[2].abs() < 1e-6 && ((normal[0] * point[0] + normal[1] * point[1]).abs() - 6.35).abs() < 1e-6 =>
|
||||
{
|
||||
spindle.push(member)
|
||||
}
|
||||
Some(FaceSurface::Cylinder { origin, axis, radius })
|
||||
if vertical(axis) && (radius - 1.5).abs() < 1e-6 && origin[0].hypot(origin[1]) < 8.0 =>
|
||||
{
|
||||
spindle.push(member)
|
||||
}
|
||||
Some(FaceSurface::Cylinder { origin, axis, radius })
|
||||
if vertical(axis) && (radius - 7.0).abs() < 1e-6 && (origin[0] - 170.0).hypot(origin[1]) < 1e-6 =>
|
||||
{
|
||||
pedal.push(member)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
[spindle, pedal]
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -664,6 +737,30 @@ mod tests {
|
||||
assert!(r["max_von_mises"]["value"].as_f64().unwrap() > 200.0, "{r}");
|
||||
}
|
||||
|
||||
/// The crank arm opens swept all-hex20 with every bore face found, and its root bends as a
|
||||
/// beam: σxx on the arm's sides at x = 29 mm (35.6 mm wide, 16 thick, 141 mm from the pedal)
|
||||
/// within 3 % of M/Z = 1500 · 141 / (16 · 35.6² / 6) = 62.6 MPa, the spindle carrying the
|
||||
/// whole load.
|
||||
#[test]
|
||||
#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]
|
||||
fn the_crank_arm_root_bends_as_the_beam_formula_says() {
|
||||
let mut s = open("crank-arm");
|
||||
let elements = s.model.elements().values().filter(|e| e.kind == fieldcraft_model::ElementKind::Hex20).count();
|
||||
assert_eq!(elements, s.model.elements().len());
|
||||
let faces = |set: &str| s.model.sets().get(set).map_or(0, |x| x.geometry.len());
|
||||
assert_eq!((faces("spindle"), faces("pedal")), (8, 2));
|
||||
s.execute("solve.run", &json!({})).unwrap();
|
||||
let half_width: f64 = 19.0 - 7.0 * 29.0 / 170.0;
|
||||
let probe = s.execute("solve.probe", &json!({"step": "Pedal", "at": [[29.0, -half_width, 8.0], [29.0, half_width, 8.0]]})).unwrap();
|
||||
let sxx = |i: usize| probe["nodes"][i]["stress"][0].as_f64().unwrap();
|
||||
let hand = PEDAL_LOAD * (170.0 - 29.0) / (16.0 * (2.0 * half_width).powi(2) / 6.0);
|
||||
for (i, sign) in [(0, -1.0), (1, 1.0)] {
|
||||
assert!((sign * sxx(i) - hand).abs() < 0.03 * hand, "{} against {hand} MPa: {probe}", sxx(i));
|
||||
}
|
||||
let reaction = s.execute("results.reactions", &json!({"step": "Pedal", "sets": ["spindle"]})).unwrap();
|
||||
assert!((reaction["sets"][0]["force"][1].as_f64().unwrap() - PEDAL_LOAD).abs() < 1e-6, "{reaction}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_drop_test_opens_ready_to_solve() {
|
||||
let s = open("drop-test");
|
||||
|
||||
@@ -111,3 +111,28 @@ peaks at 295 MPa at the upright's hole.
|
||||
**Check:** the tip of the foot moves 0.509 mm down, within 1 % of a 48 016-element tet10
|
||||
solution of the unpartitioned part (0.5093 mm, `crates/testkit/tests/bracket_blocks.rs`), and the
|
||||
clamp carries the whole 1 kN.
|
||||
|
||||
## Bicycle crank arm (`crank-arm`)
|
||||
|
||||

|
||||
|
||||
A 170 mm aluminium crank built in SolidCraft (`crates/engine/samples/crank-arm.json`): a 16 mm
|
||||
arm tapering from 38 to 24 mm, root arcs into a Ø46 × 22 spindle boss with a 12.7 mm square bore
|
||||
(R1.5 corners) and a Ø30 × 20 pedal boss with a Ø14 hole, and an 82 × 14 × 8 pocket in its back
|
||||
(R5 corners). It is a stack of extrusions along z, so `mesh.hex.generate` sweeps it all-hex:
|
||||
7 274 hex20 elements of 3 mm, all valid, scaled Jacobian min 0.17, mean 0.83. To keep it
|
||||
sweepable the hex variant leaves out the pocket's floor fillet and the bosses' 1.5 mm chamfers,
|
||||
and its root arcs meet the bosses about 20° off tangent (a tangent meeting leaves a zero-angle
|
||||
cusp on the arm's top face, where any hex is degenerate). 6061-T6 (E = 68 900 MPa, ν = 0.33); the
|
||||
square bore's faces fixed, 1500 N on the pedal hole in the crank's plane, across the arm (−y).
|
||||
Linear static: about 4 s to open and 7 s to solve.
|
||||
|
||||
**Look at:** the swept mesh (Prepare ▸ Mesh), eight layers through the arm, and the von Mises
|
||||
contours: the bending stress grows along the arm toward the root; the peak, 129 MPa, sits at a
|
||||
square-bore corner where the rigid clamp meets the free face, a support singularity.
|
||||
|
||||
**Check:** σxx on the arm's sides at x = 29 mm is ±62.4 MPa against the beam formula
|
||||
M/Z = 1500 · 141 / (16 · 35.6² / 6) = 62.6 MPa (0.3 %); a 41 815-element tet10 solution of the
|
||||
full part gives 62.3 MPa. The pedal bore moves 0.75 mm down (tet10: 0.70–0.75 mm; Euler–Bernoulli
|
||||
with a rigid clamp: 0.58 mm, without shear or the boss's compliance), and the spindle carries the
|
||||
whole 1500 N and 255 N·m.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 572 KiB |
Reference in new issue
Block a user