Files
OneCAD-Simulation/crates/decks/tests/planar.rs
T
4c0fcc80ad 2D elements: plane stress, plane strain and axisymmetric sections on the DOF framework; mesh.planar (M4) (#39)
Model: planar section kinds with Extent, element edges in sets, edge-weighted 2D loads,
centrifugal load, 2D model checks. fem: Plane kernels on Basis::{Tri3,Tri6,Quad4,Quad8}
declaring ElementDofs::new(DofSet::PLANE); QM6 incompatible modes with the
Wilson-Ibrahimbegovic correction. Solver: Kernel enum on the DofLayout; 3D results
bit-identical (hash-checked). Decks: Abaqus CPS/CPE/CAX, Nastran membranes and RFORCE,
LS-DYNA ELFORM 12/13. Mesh: free triangle mesher, planar region meshing, face boundaries.
Engine: mesh.planar with tagged edges. Analytic verification suite in solver/tests/plane.rs.


Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:24:56 -04:00

158 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 modelcraft_decks::{Error, abaqus, edge_pressure_loads, lsdyna, nastran};
use modelcraft_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), fluid: None })
.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}");
}