Files
OneCAD-Simulation/crates/decks/tests/planar.rs
T
jyvikoandClaude Opus 5.5 d5bd74570f Rename the project to FieldCraft
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
2026-10-13 23:51:40 -04:00

161 lines
9.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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}");
}