forked from 1b-agent/fieldcraft-upstream
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:
42 files changed
+3270
-128
No files matched your search
@@ -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,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"]);
|
||||
|
||||
Reference in new issue
Block a user