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>
This commit is contained in:
authored and GitHub committed 2026-10-13 20:24:56 -04:00
1 parent 4e4d19b88c
commit 4c0fcc80ad
42 files changed
+3270 -128

No files matched your search

+15 -1
View File
@@ -97,7 +97,9 @@ pub(crate) struct Elform {
/// hexahedra), reduced 15 (two-point pentahedron); tet4 10 (one point, exact for a linear
/// tetrahedron), hybrid 13 (nodal pressure); tet10 16; hex20 23. Shells: quad4 16 (fully
/// integrated), reduced 2 (one-point) with hourglass control; tri3 4 (C0 triangle); quad8 23;
/// tri6 24. Rods: 3 (truss).
/// tri6 24. Rods: 3 (truss). 2D (in the XY plane): plane stress 12, plane strain 13, both one-point
/// forms on quad4 (written for reduced integration, with hourglass control) and tri3 (exact at one
/// point); quadratic 2D elements and axisymmetric sections have no form written here.
/// What it cannot express is the reason, naming the section.
pub(crate) fn elform(kind: ElementKind, section: &SectionKind, f: Formulation, section_name: &str) -> Result<Elform, String> {
let plain = |elform| Ok(Elform { elform, hourglass: None });
@@ -142,6 +144,18 @@ pub(crate) fn elform(kind: ElementKind, section: &SectionKind, f: Formulation, s
_ => no("a rod section cannot hold"),
},
SectionKind::Beam { .. } => no("beam sections are not written to LS-DYNA decks yet (planned with beam profiles):"),
SectionKind::Axisymmetric {} => {
no("axisymmetric sections are not written to LS-DYNA decks yet (its axisymmetric forms count loads per radian; not verified):")
}
SectionKind::PlaneStress { .. } | SectionKind::PlaneStrain { .. } => {
let form = if matches!(section, SectionKind::PlaneStress { .. }) { 12 } else { 13 };
match kind {
ElementKind::Tri3 => plain(form),
ElementKind::Quad4 if reduced => Ok(Elform { elform: form, hourglass: Some(Hourglass::Shell) }),
ElementKind::Quad4 => no("LS-DYNA's 2D forms integrate at one point (set the section's integration to reduced) for"),
_ => no("LS-DYNA's 2D forms are linear; there is no form for"),
}
}
}
}
+30 -1
View File
@@ -30,6 +30,11 @@
//! order when LS-DYNA is started with `CASE` on its command line. A single step is written
//! without cases.
//!
//! 2D models (plane stress and plane strain in the XY plane) are `*SECTION_SHELL` parts with
//! ELFORM 12 and 13 ([`elements`]); plane strain is per unit thickness in LS-DYNA, so only a
//! thickness of 1 is written; pressures on 2D element edges are the consistent nodal forces
//! (`*LOAD_NODE_POINT`).
//!
//! What LS-DYNA cannot take is an [`Error::Unsupported`] naming the item: beam sections (not yet
//! written), body forces per volume, kinematic couplings of some degrees of freedom only, boundary
//! conditions on a coupled reference point, gravity on different sets, hybrid formulations other
@@ -188,6 +193,11 @@ fn plan<'a>(model: &'a Model, r: &Resolved) -> Result<Plan<'a>> {
let el = model.element(*e).ok_or_else(|| broken(format!("element {e} of section `{name}`")))?;
by_kind.entry(el.kind).or_default().push((*e, el));
}
if let SectionKind::PlaneStrain { thickness } = sec.kind
&& thickness != 1.0
{
refused.insert(format!("section `{name}`: a plane strain thickness of {thickness} (LS-DYNA's plane strain is per unit thickness)"));
}
if let SectionKind::Shell { points, .. } = sec.kind
&& points > MAX_SHELL_POINTS
{
@@ -270,9 +280,13 @@ fn plan<'a>(model: &'a Model, r: &Resolved) -> Result<Plan<'a>> {
}
}
Load::Traction { .. } => {}
Load::Pressure { .. } if r.is_planar() => {}
Load::Pressure { set, .. } => {
segment_sets.insert(set.as_str());
}
Load::Centrifugal { .. } => {
refused.insert(format!("load `{l}`: a centrifugal load (not written to LS-DYNA decks yet)"));
}
Load::Gravity { set, .. } => {
gravity_sets.insert(set.as_deref());
}
@@ -414,6 +428,14 @@ fn parts(d: &mut Deck, plan: &Plan) -> Result<()> {
d.card([Field::from(area)]);
}
SectionKind::Beam { .. } => return Err(broken(format!("section `{}`: beams have no LS-DYNA formulation", p.name))),
SectionKind::PlaneStress { thickness } | SectionKind::PlaneStrain { thickness } => {
d.line("*SECTION_SHELL");
d.header(&["secid", "elform"]);
d.card([Field::from(p.id), Field::from(p.elform)]);
d.header(&["t1", "t2", "t3", "t4"]);
d.card([Field::from(thickness), Field::from(thickness), Field::from(thickness), Field::from(thickness)]);
}
SectionKind::Axisymmetric {} => return Err(broken(format!("section `{}`: axisymmetric sections have no LS-DYNA formulation", p.name))),
}
}
for (h, hgid) in &plan.hourglasses {
@@ -621,12 +643,14 @@ fn write_step(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan, step: &Ste
if is_static {
let mut gravity = [0.0f64; 3];
let mut pointwise = Vec::new();
let mut edge_pressures = Vec::new();
for (name, load) in &loads {
match load {
Load::Force { .. } | Load::Moment { .. } if plan.uniform.contains_key(name.as_str()) => {
on_node_set(d, plan, name, matches!(load, Load::Moment { .. }), n);
}
Load::Force { .. } | Load::Moment { .. } | Load::Traction { .. } => pointwise.push(*name),
Load::Pressure { .. } if r.is_planar() => edge_pressures.push(name.as_str()),
Load::Pressure { set, magnitude } => {
let sid = plan.sets.get(set.as_str()).copied().unwrap_or_default();
d.comment(&format!("load `{name}`"));
@@ -640,10 +664,15 @@ fn write_step(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan, step: &Ste
}
}
Load::BodyForce { .. } => return Err(broken(format!("load `{name}`: a body force"))),
Load::Centrifugal { .. } => return Err(broken(format!("load `{name}`: a centrifugal load"))),
}
}
// The other concentrated loads node by node, summed where they share a node.
let point = crate::concentrated_loads(model, r, pointwise);
let mut point = crate::concentrated_loads(model, r, pointwise);
for (key, v) in crate::edge_pressure_loads(model, r, edge_pressures)? {
*point.entry(key).or_default() += v;
}
point.retain(|_, v| *v != 0.0);
if !point.is_empty() {
d.line("*LOAD_NODE_POINT");
d.header(&["nid", "dof", "lcid", "sf"]);