forked from 1b-agent/fieldcraft-upstream
Crates fieldcraft-*, binaries fieldcraft and fieldcraft-cli, the .fieldcraft file (format tag fieldcraft/1, manifest fieldcraft.json), FIELDCRAFT_* environment variables, the MCP server name, egui widget ids (fc_*), the app icon document FieldCraft.icon and its generated family, docs and licences. No compatibility shim: no files of the earlier name exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
161 lines
9.1 KiB
Rust
161 lines
9.1 KiB
Rust
//! 2D models (plane stress, plane strain, axisymmetric) in every deck format: element names,
|
||
//! section data, edge surfaces and pressures, centrifugal loads, and what a format cannot write.
|
||
|
||
use fieldcraft_decks::{Error, abaqus, edge_pressure_loads, lsdyna, nastran};
|
||
use fieldcraft_model::{
|
||
BoundaryCondition, Elastic, ElementEdge, ElementKind, Formulation, Integration, Load, Material, Model, Procedure, Section, SectionKind, Set,
|
||
Step, Target, UnitSystem,
|
||
};
|
||
|
||
/// Two quad4 over [1, 3] × [0, 1] (or the given kind on the same corners), the left edge held,
|
||
/// a pressure on the right edge and a centrifugal load.
|
||
fn plate(kind: SectionKind, element: ElementKind, formulation: Formulation) -> Model {
|
||
let mut m = Model::default();
|
||
m.set_units(UnitSystem::MmTonneS);
|
||
for (i, x) in [1.0, 2.0, 3.0].iter().enumerate() {
|
||
m.set_node(i as u32 + 1, [*x, 0.0, 0.0]).unwrap();
|
||
m.set_node(i as u32 + 4, [*x, 1.0, 0.0]).unwrap();
|
||
}
|
||
match element {
|
||
ElementKind::Tri3 => {
|
||
for (l, n) in [(1, [1, 2, 5]), (2, [1, 5, 4]), (3, [2, 3, 6]), (4, [2, 6, 5])] {
|
||
m.set_element(l, ElementKind::Tri3, n.to_vec()).unwrap();
|
||
}
|
||
}
|
||
_ => {
|
||
m.set_element(1, ElementKind::Quad4, vec![1, 2, 5, 4]).unwrap();
|
||
m.set_element(2, ElementKind::Quad4, vec![2, 3, 6, 5]).unwrap();
|
||
}
|
||
}
|
||
let right = if element == ElementKind::Tri3 { ElementEdge { element: 3, edge: 2 } } else { ElementEdge { element: 2, edge: 2 } };
|
||
let all: Vec<u32> = m.elements().keys().copied().collect();
|
||
m.add_set("all", Set { elements: all, ..Set::default() }).unwrap();
|
||
m.add_set("left", Set { nodes: vec![1, 4], ..Set::default() }).unwrap();
|
||
m.add_set("right", Set { edges: vec![right], ..Set::default() }).unwrap();
|
||
m.add_material(
|
||
"Steel",
|
||
Material { elastic: Some(Elastic::Isotropic { young: 210_000.0, poisson: 0.3 }), density: Some(7.85e-9), ..Material::default() },
|
||
)
|
||
.unwrap();
|
||
m.add_section("plate", Section { set: "all".into(), material: "Steel".into(), kind, formulation }).unwrap();
|
||
m.add_boundary_condition("hold", BoundaryCondition::Fixed { target: Target::Set("left".into()) }).unwrap();
|
||
m.add_load("press", Load::Pressure { set: "right".into(), magnitude: 5.0 }).unwrap();
|
||
let axis = if kind == (SectionKind::Axisymmetric {}) { [0.0, 1.0, 0.0] } else { [0.0, 0.0, 1.0] };
|
||
m.add_load("spin", Load::Centrifugal { angular_velocity: 100.0, point: [0.0; 3], axis, set: None }).unwrap();
|
||
let mut s = Step::new("Load", Procedure::Static {});
|
||
s.boundary_conditions.insert("hold".into());
|
||
s.loads.extend(["press".to_string(), "spin".to_string()]);
|
||
m.add_step(s, None).unwrap();
|
||
m
|
||
}
|
||
|
||
fn unsupported(r: Result<String, Error>) -> String {
|
||
match r {
|
||
Err(Error::Unsupported { what, .. }) => what,
|
||
other => panic!("expected unsupported, got {other:?}"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn abaqus_writes_plane_and_axisymmetric_elements() {
|
||
let full = Formulation::default();
|
||
let im = Formulation { incompatible_modes: true, ..Formulation::default() };
|
||
let red = Formulation { integration: Integration::Reduced, ..Formulation::default() };
|
||
for (kind, f, want) in [
|
||
(SectionKind::PlaneStress { thickness: 2.0 }, full, "CPS4"),
|
||
(SectionKind::PlaneStress { thickness: 2.0 }, im, "CPS4I"),
|
||
(SectionKind::PlaneStrain { thickness: 1.0 }, red, "CPE4R"),
|
||
(SectionKind::Axisymmetric {}, im, "CAX4I"),
|
||
] {
|
||
let deck = abaqus::write(&plate(kind, ElementKind::Quad4, f)).unwrap();
|
||
assert!(deck.contains(&format!("*ELEMENT, TYPE={want}\n")), "{want}\n{deck}");
|
||
// The right edge is surface S2 of element 2; the pressure is a surface load on it.
|
||
assert!(deck.contains("*SURFACE, TYPE=ELEMENT, NAME=right\n2, S2\n"), "{deck}");
|
||
assert!(deck.contains("right, P, 5.0\n"), "{deck}");
|
||
assert!(deck.contains("all, CENTRIF, 10000.0, 0.0, 0.0, 0.0, "), "{deck}");
|
||
let section = match kind {
|
||
SectionKind::Axisymmetric {} => "*SOLID SECTION, ELSET=all, MATERIAL=Steel\n,\n",
|
||
SectionKind::PlaneStress { .. } => "*SOLID SECTION, ELSET=all, MATERIAL=Steel\n2.0\n",
|
||
_ => "*SOLID SECTION, ELSET=all, MATERIAL=Steel\n1.0\n",
|
||
};
|
||
assert!(deck.contains(section), "{deck}");
|
||
}
|
||
let deck = abaqus::write(&plate(SectionKind::PlaneStrain { thickness: 1.0 }, ElementKind::Tri3, full)).unwrap();
|
||
assert!(deck.contains("*ELEMENT, TYPE=CPE3\n") && deck.contains("3, S2\n"), "{deck}");
|
||
// Axisymmetric body forces are radial and axial.
|
||
let mut m = plate(SectionKind::Axisymmetric {}, ElementKind::Quad4, full);
|
||
m.add_load("swell", Load::BodyForce { set: "all".into(), vector: [1.0, 2.0, 0.0] }).unwrap();
|
||
m.set_load_active("Load", "swell", true).unwrap();
|
||
let deck = abaqus::write(&m).unwrap();
|
||
assert!(deck.contains("all, BR, 1.0\nall, BZ, 2.0\n"), "{deck}");
|
||
// Abaqus has no reduced six-node triangle.
|
||
let mut m = plate(SectionKind::PlaneStress { thickness: 1.0 }, ElementKind::Quad4, red);
|
||
for (l, n) in [(10, 10), (11, 11), (12, 12), (13, 13), (14, 14), (15, 15)] {
|
||
m.set_node(l, [1.0 + f64::from(n - 10) * 0.1, 2.0 + f64::from(n % 2), 0.0]).unwrap();
|
||
}
|
||
m.set_element(9, ElementKind::Tri6, vec![10, 12, 14, 11, 13, 15]).unwrap();
|
||
m.replace_set("all", Set { elements: vec![1, 2, 9], ..Set::default() }).unwrap();
|
||
assert!(abaqus::write(&m).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn nastran_writes_plane_stress_membranes_and_refuses_the_rest() {
|
||
let m = plate(SectionKind::PlaneStress { thickness: 2.0 }, ElementKind::Quad4, Formulation::default());
|
||
let deck = nastran::write(&m).unwrap();
|
||
// A membrane: the material on MID1 only.
|
||
assert!(deck.contains("PSHELL* 1 1 2.\n$"), "{deck}");
|
||
assert!(deck.contains("CQUAD4 1 1 1 2 5 4"), "{deck}");
|
||
// Every grid holds its out-of-plane degrees of freedom (PS on the continuation).
|
||
let grids = deck.lines().filter(|l| l.starts_with("GRID")).count();
|
||
assert_eq!(grids, 6);
|
||
assert_eq!(deck.lines().filter(|l| l.starts_with('*') && l.trim_end().ends_with("3456")).count(), grids, "{deck}");
|
||
// The edge pressure as consistent forces, pushing in (−X on the right edge).
|
||
let forces: Vec<&str> = deck.lines().filter(|l| l.starts_with("FORCE")).collect();
|
||
assert_eq!(forces.len(), 2, "{deck}");
|
||
assert!(deck.contains("RFORCE"), "{deck}");
|
||
for kind in [SectionKind::PlaneStrain { thickness: 1.0 }, SectionKind::Axisymmetric {}] {
|
||
let why = unsupported(nastran::write(&plate(kind, ElementKind::Quad4, Formulation::default())));
|
||
assert!(why.contains("differ between dialects"), "{why}");
|
||
}
|
||
let mut m = plate(SectionKind::PlaneStress { thickness: 2.0 }, ElementKind::Quad4, Formulation::default());
|
||
m.replace_load("spin", Load::Centrifugal { angular_velocity: 1.0, point: [5.0, 0.0, 0.0], axis: [0.0, 0.0, 1.0], set: None }).unwrap();
|
||
assert!(unsupported(nastran::write(&m)).contains("misses the origin"));
|
||
}
|
||
|
||
#[test]
|
||
fn lsdyna_writes_one_point_plane_forms_and_refuses_the_rest() {
|
||
let red = Formulation { integration: Integration::Reduced, ..Formulation::default() };
|
||
let mut m = plate(SectionKind::PlaneStress { thickness: 2.0 }, ElementKind::Quad4, red);
|
||
m.remove_step("Load").unwrap();
|
||
let mut s = Step::new("Load", Procedure::Static {});
|
||
s.boundary_conditions.insert("hold".into());
|
||
s.loads.insert("press".into());
|
||
m.add_step(s, None).unwrap();
|
||
let deck = lsdyna::write(&m).unwrap().text;
|
||
assert!(deck.contains("*SECTION_SHELL") && deck.contains("*HOURGLASS"), "{deck}");
|
||
assert!(deck.contains("*LOAD_NODE_POINT") && !deck.contains("*LOAD_SEGMENT"), "{deck}");
|
||
let why = |m: &Model| match lsdyna::write(m) {
|
||
Err(Error::Unsupported { what, .. }) => what,
|
||
other => panic!("{other:?}"),
|
||
};
|
||
assert!(why(&plate(SectionKind::PlaneStress { thickness: 2.0 }, ElementKind::Quad4, Formulation::default())).contains("one point"));
|
||
assert!(why(&plate(SectionKind::Axisymmetric {}, ElementKind::Tri3, red)).contains("axisymmetric"));
|
||
assert!(why(&plate(SectionKind::PlaneStrain { thickness: 2.0 }, ElementKind::Tri3, red)).contains("per unit thickness"));
|
||
assert!(why(&plate(SectionKind::PlaneStrain { thickness: 1.0 }, ElementKind::Tri3, red)).contains("centrifugal"));
|
||
}
|
||
|
||
#[test]
|
||
fn edge_pressures_are_the_consistent_nodal_forces() {
|
||
// Pressure 5 on the right edge (x = 3, length 1): plane stress t = 2 gives −5 per node in X;
|
||
// axisymmetric, the ring 2π · 3 · 1 shares 5 · 6π equally on a straight edge at fixed r.
|
||
let m = plate(SectionKind::PlaneStress { thickness: 2.0 }, ElementKind::Quad4, Formulation::default());
|
||
let r = m.resolve().unwrap();
|
||
let f = edge_pressure_loads(&m, &r, ["press"]).unwrap();
|
||
assert_eq!(f.len(), 2);
|
||
assert!(f.values().all(|v| (v + 5.0).abs() < 1e-12), "{f:?}");
|
||
let m = plate(SectionKind::Axisymmetric {}, ElementKind::Quad4, Formulation::default());
|
||
let r = m.resolve().unwrap();
|
||
let total: f64 = edge_pressure_loads(&m, &r, ["press"]).unwrap().values().sum();
|
||
assert!((total + 5.0 * 6.0 * std::f64::consts::PI).abs() < 1e-10, "{total}");
|
||
}
|