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 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
42 files changed
+3270
-128
No files matched your search
+88
-10
@@ -64,6 +64,18 @@
|
||||
//! | quad8 | | S8R (reduced only) | | |
|
||||
//! | beam2, rod2 | | | B31 | T3D2 |
|
||||
//!
|
||||
//! | ModelCraft | Plane stress | Plane strain | Axisymmetric |
|
||||
//! |---|---|---|---|
|
||||
//! | tri3 | CPS3 | CPE3 | CAX3 |
|
||||
//! | tri6 | CPS6 | CPE6 | CAX6 |
|
||||
//! | quad4 | CPS4, CPS4R, CPS4I | CPE4, CPE4R, CPE4I | CAX4, CAX4R, CAX4I |
|
||||
//! | quad8 | CPS8, CPS8R | CPE8, CPE8R | CAX8, CAX8R |
|
||||
//!
|
||||
//! 2D models: Abaqus's axisymmetric elements, like the model, take r = x and the axis along Y,
|
||||
//! and their concentrated loads are totals over the circumference; a 2D section's thickness is
|
||||
//! the `*SOLID SECTION` data line. Surfaces of 2D elements are their edges, `S1`… in the edge
|
||||
//! order of the model; body forces of axisymmetric elements are `BR`, `BZ`.
|
||||
//!
|
||||
//! `H` marks the hybrid variant. A four-node tetrahedron integrates at one point either way, so
|
||||
//! reduced integration writes C3D4. Not expressible: reduced integration of ten-node tetrahedra and
|
||||
//! six-node wedges, full integration of eight-node shells, six-node triangular shells, orthotropic
|
||||
@@ -125,9 +137,28 @@ pub fn element_type(kind: ElementKind, section: &Section) -> std::result::Result
|
||||
use SectionKind as S;
|
||||
let f = section.formulation;
|
||||
let reduced = f.integration == Integration::Reduced;
|
||||
if (f.hybrid || f.incompatible_modes) && section.kind.dimension() != 3 {
|
||||
if (f.hybrid && section.kind.dimension() != 3) || (f.incompatible_modes && !section.kind.is_continuum()) {
|
||||
return Err(format!("hybrid or incompatible-mode {} sections", section.kind.name()));
|
||||
}
|
||||
if let Some(prefix) = match section.kind {
|
||||
S::PlaneStress { .. } => Some("CPS"),
|
||||
S::PlaneStrain { .. } => Some("CPE"),
|
||||
S::Axisymmetric {} => Some("CAX"),
|
||||
_ => None,
|
||||
} {
|
||||
let suffix = match (kind, reduced, f.incompatible_modes) {
|
||||
(K::Quad4, false, true) => "4I",
|
||||
(_, true, true) => return Err("incompatible modes with reduced integration".into()),
|
||||
(_, _, true) => return Err(format!("incompatible modes on {} elements (quad4 only)", kind.name())),
|
||||
(K::Tri3, ..) => "3",
|
||||
(K::Tri6, true, _) => return Err("reduced integration of tri6 elements (Abaqus has no reduced-integration variant)".into()),
|
||||
(K::Tri6, ..) => "6",
|
||||
(K::Quad4, r, _) => ["4", "4R"][usize::from(r)],
|
||||
(K::Quad8, r, _) => ["8", "8R"][usize::from(r)],
|
||||
_ => return Err(format!("{} elements in a {} section", kind.name(), section.kind.name())),
|
||||
};
|
||||
return Ok(format!("{prefix}{suffix}"));
|
||||
}
|
||||
let plain = match (§ion.kind, kind, reduced, f.incompatible_modes) {
|
||||
(S::Solid {}, K::Hex8, false, true) => "C3D8I",
|
||||
(S::Solid {}, _, true, true) => return Err("incompatible modes with reduced integration".into()),
|
||||
@@ -209,7 +240,7 @@ fn check_expressible(model: &Model) -> Result<()> {
|
||||
}
|
||||
for (name, section) in model.sections() {
|
||||
let ortho = matches!(model.material(§ion.material).and_then(|m| m.elastic), Some(Elastic::EngineeringConstants { .. }));
|
||||
if ortho && section.kind.dimension() != 3 {
|
||||
if ortho && !section.kind.is_continuum() {
|
||||
bad.insert(format!(
|
||||
"section `{name}`: orthotropic material `{}` in a {} section (its material axes need an orientation, not supported yet)",
|
||||
section.material,
|
||||
@@ -268,10 +299,22 @@ pub fn face_id(kind: ElementKind, face: u8) -> Option<&'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The surface identifier of edge `edge` (1-based) of a 2D element: `S1`… in the model's edge
|
||||
/// order (which is Abaqus's face order for plane and axisymmetric elements).
|
||||
pub fn edge_id(kind: ElementKind, edge: u8) -> Option<&'static str> {
|
||||
const EDGES: [&str; 4] = ["S1", "S2", "S3", "S4"];
|
||||
let i = usize::from(edge).checked_sub(1)?;
|
||||
if kind.dimension() != 2 || i >= kind.edges().len() {
|
||||
return None;
|
||||
}
|
||||
EDGES.get(i).copied()
|
||||
}
|
||||
|
||||
/// The node surface each coupling ties its reference point to: a fresh name per coupling that
|
||||
/// clashes with no set (sets with faces own surfaces of their name) and no other coupling surface.
|
||||
fn coupling_surfaces(model: &Model, r: &Resolved) -> BTreeMap<String, String> {
|
||||
let mut taken: BTreeSet<String> = r.sets.iter().filter(|(_, s)| !s.faces.is_empty()).map(|(n, _)| n.to_ascii_uppercase()).collect();
|
||||
let mut taken: BTreeSet<String> =
|
||||
r.sets.iter().filter(|(n, s)| !s.faces.is_empty() || !r.planar_edges(n).is_empty()).map(|(n, _)| n.to_ascii_uppercase()).collect();
|
||||
let mut out = BTreeMap::new();
|
||||
for name in model.couplings().keys() {
|
||||
let base = format!("{name}-nodes");
|
||||
@@ -365,6 +408,9 @@ fn element_loads<'a>(loads: &[ElementLoad<'a>], kind: BodyKind, out: &mut Vec<(E
|
||||
out.retain(|(_, _, v)| v.iter().any(|c| *c != 0.0));
|
||||
}
|
||||
|
||||
/// A centrifugal load: the element set, ω², a point on the axis and its direction.
|
||||
type Centrifugal<'a> = (&'a str, f64, [f64; 3], [f64; 3]);
|
||||
|
||||
/// The loads of one general step, as the deck writes them.
|
||||
#[derive(Default)]
|
||||
struct StepLoads<'a> {
|
||||
@@ -373,6 +419,9 @@ struct StepLoads<'a> {
|
||||
/// Surface (set name) → summed pressure.
|
||||
pressure: BTreeMap<&'a str, f64>,
|
||||
element: Vec<(ElementTarget<'a>, BodyKind, [f64; 3])>,
|
||||
centrifugal: Vec<Centrifugal<'a>>,
|
||||
/// Body forces of axisymmetric elements are radial and axial (`BR`, `BZ`).
|
||||
axisymmetric: bool,
|
||||
}
|
||||
|
||||
/// Which load keywords a step wrote entries for, so the next step knows what to remove.
|
||||
@@ -387,7 +436,8 @@ impl<'a> StepLoads<'a> {
|
||||
/// `everything` is every element and the section sets that hold them (gravity on the whole
|
||||
/// model).
|
||||
fn new(model: &'a Model, r: &'a Resolved, step: &'a Step, everything: &'a (Vec<&'a str>, Vec<u32>)) -> StepLoads<'a> {
|
||||
let mut s = StepLoads { concentrated: crate::concentrated_loads(model, r, &step.loads), ..StepLoads::default() };
|
||||
let axisymmetric = r.extent() == Some(modelcraft_model::Extent::Revolved);
|
||||
let mut s = StepLoads { concentrated: crate::concentrated_loads(model, r, &step.loads), axisymmetric, ..StepLoads::default() };
|
||||
let (mut gravity, mut body) = (Vec::new(), Vec::new());
|
||||
for name in &step.loads {
|
||||
match model.load(name) {
|
||||
@@ -397,6 +447,13 @@ impl<'a> StepLoads<'a> {
|
||||
gravity.push(ElementLoad { elsets: everything.0.clone(), elements: &everything.1, vector: *acceleration })
|
||||
}
|
||||
Some(Load::BodyForce { set, vector }) => body.push(ElementLoad::on_set(r, set, *vector)),
|
||||
Some(Load::Centrifugal { angular_velocity, point, axis, set }) => {
|
||||
let w2 = angular_velocity * angular_velocity;
|
||||
match set {
|
||||
Some(set) => s.centrifugal.push((set, w2, *point, *axis)),
|
||||
None => s.centrifugal.extend(everything.0.iter().map(|set| (*set, w2, *point, *axis))),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -407,7 +464,11 @@ impl<'a> StepLoads<'a> {
|
||||
}
|
||||
|
||||
fn written(&self) -> Written {
|
||||
Written { concentrated: !self.concentrated.is_empty(), pressure: !self.pressure.is_empty(), element: !self.element.is_empty() }
|
||||
Written {
|
||||
concentrated: !self.concentrated.is_empty(),
|
||||
pressure: !self.pressure.is_empty(),
|
||||
element: !self.element.is_empty() || !self.centrifugal.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,7 +574,8 @@ impl Deck {
|
||||
self.line(&format!("*ELSET, ELSET={name}"));
|
||||
self.labels(&s.elements);
|
||||
}
|
||||
if !s.faces.is_empty() {
|
||||
let edges = r.planar_edges(name);
|
||||
if !s.faces.is_empty() || !edges.is_empty() {
|
||||
self.line(&format!("*SURFACE, TYPE=ELEMENT, NAME={name}"));
|
||||
for f in &s.faces {
|
||||
let id = model.element(f.element).and_then(|e| face_id(e.kind, f.face)).ok_or_else(|| Error::Unsupported {
|
||||
@@ -522,6 +584,13 @@ impl Deck {
|
||||
})?;
|
||||
let _ = writeln!(self.out, "{}, {id}", f.element);
|
||||
}
|
||||
for g in &edges {
|
||||
let id = model.element(g.element).and_then(|e| edge_id(e.kind, g.edge)).ok_or_else(|| Error::Unsupported {
|
||||
format: FORMAT,
|
||||
what: format!("set `{name}`: element {} has no edge {}", g.element, g.edge),
|
||||
})?;
|
||||
let _ = writeln!(self.out, "{}, {id}", g.element);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -565,10 +634,14 @@ impl Deck {
|
||||
self.comment(&format!("section {name}"));
|
||||
let (set, material) = (&s.set, &s.material);
|
||||
match s.kind {
|
||||
SectionKind::Solid {} => {
|
||||
SectionKind::Solid {} | SectionKind::Axisymmetric {} => {
|
||||
self.line(&format!("*SOLID SECTION, ELSET={set}, MATERIAL={material}"));
|
||||
self.line(",");
|
||||
}
|
||||
SectionKind::PlaneStress { thickness } | SectionKind::PlaneStrain { thickness } => {
|
||||
self.line(&format!("*SOLID SECTION, ELSET={set}, MATERIAL={material}"));
|
||||
self.numbers_line("", &[thickness])?;
|
||||
}
|
||||
SectionKind::Rod { area } => {
|
||||
self.line(&format!("*SOLID SECTION, ELSET={set}, MATERIAL={material}"));
|
||||
self.numbers_line("", &[area])?;
|
||||
@@ -686,7 +759,7 @@ impl Deck {
|
||||
self.numbers_line(&format!("{surface}, P"), &[*p])?;
|
||||
}
|
||||
}
|
||||
if self.op_new("*DLOAD", !loads.element.is_empty(), before.element) {
|
||||
if self.op_new("*DLOAD", !loads.element.is_empty() || !loads.centrifugal.is_empty(), before.element) {
|
||||
for (target, kind, v) in &loads.element {
|
||||
let target = match target {
|
||||
ElementTarget::Set(s) => s.to_string(),
|
||||
@@ -698,7 +771,8 @@ impl Deck {
|
||||
self.numbers_line(&format!("{target}, GRAV"), &[g, v[0] / g, v[1] / g, v[2] / g])?;
|
||||
}
|
||||
BodyKind::Force => {
|
||||
for (label, c) in ["BX", "BY", "BZ"].into_iter().zip(v) {
|
||||
let labels: &[&str] = if loads.axisymmetric { &["BR", "BZ"] } else { &["BX", "BY", "BZ"] };
|
||||
for (label, c) in labels.iter().zip(v) {
|
||||
if *c != 0.0 {
|
||||
self.numbers_line(&format!("{target}, {label}"), &[*c])?;
|
||||
}
|
||||
@@ -706,6 +780,10 @@ impl Deck {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (set, w2, point, axis) in &loads.centrifugal {
|
||||
// CENTRIF: ω², a point on the axis and the axis direction.
|
||||
self.numbers_line(&format!("{set}, CENTRIF"), &[*w2, point[0], point[1], point[2], axis[0], axis[1], axis[2]])?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -746,7 +824,7 @@ impl Deck {
|
||||
self.boundary_lines(model, r, &initial)?;
|
||||
}
|
||||
// Gravity on the whole model acts on every element, through the section sets.
|
||||
let whole_model_gravity = model.loads().values().any(|l| matches!(l, Load::Gravity { set: None, .. }));
|
||||
let whole_model_gravity = model.loads().values().any(|l| matches!(l, Load::Gravity { set: None, .. } | Load::Centrifugal { set: None, .. }));
|
||||
let everything: (Vec<&str>, Vec<u32>) = if whole_model_gravity {
|
||||
(model.sections().values().map(|s| s.set.as_str()).collect(), model.elements().keys().copied().collect())
|
||||
} else {
|
||||
|
||||
@@ -15,6 +15,7 @@ pub mod nastran;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use modelcraft_model::nodal::edge_pressure;
|
||||
use modelcraft_model::{Diagnostic, Load, Model, Resolved};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
@@ -134,3 +135,26 @@ pub fn concentrated_loads<'a>(model: &Model, r: &Resolved, active: impl IntoIter
|
||||
out.retain(|_, v| *v != 0.0);
|
||||
out
|
||||
}
|
||||
|
||||
/// The consistent nodal forces (degrees of freedom 1 and 2) of the active pressures on a 2D
|
||||
/// model's element edges, for formats that write them as concentrated loads: the model's edge
|
||||
/// integration ([`modelcraft_model::nodal::edge_pressure`]), a positive pressure pushing
|
||||
/// into the element, summed per node and direction; zero sums are left out.
|
||||
pub fn edge_pressure_loads<'a>(model: &Model, r: &Resolved, active: impl IntoIterator<Item = &'a str>) -> Result<BTreeMap<(u32, u8), f64>> {
|
||||
let mut out: BTreeMap<(u32, u8), f64> = BTreeMap::new();
|
||||
for name in active {
|
||||
let Some(Load::Pressure { set, magnitude }) = model.load(name) else { continue };
|
||||
for g in r.planar_edges(set) {
|
||||
let Some(extent) = r.planar.get(&g.element).copied() else { continue };
|
||||
let labels = model.edge_nodes(g).unwrap_or_default();
|
||||
let xyz: Vec<[f64; 3]> = labels.iter().filter_map(|n| model.node(*n)).map(|n| n.xyz).collect();
|
||||
for (node, f) in labels.iter().zip(edge_pressure(&xyz, extent, *magnitude)?) {
|
||||
for (dof, c) in (1u8..).zip(&f[..2]) {
|
||||
*out.entry((*node, dof)).or_default() += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.retain(|_, v| *v != 0.0);
|
||||
Ok(out)
|
||||
}
|
||||
@@ -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"]);
|
||||
|
||||
@@ -24,7 +24,15 @@
|
||||
//! gravity: `GRAV`; a step with several loads combines them with `LOAD`;
|
||||
//! - kinematic coupling: `RBE2` (the reference point independent); distributing coupling: `RBE3`
|
||||
//! (equal weights on the set's translations; the reference point dependent);
|
||||
//! - frequency steps: `EIGRL` (Lanczos) with the step's mode count and range.
|
||||
//! - frequency steps: `EIGRL` (Lanczos) with the step's mode count and range;
|
||||
//! - 2D models: plane stress as membranes, `CTRIA3`, `CQUAD4`, `CTRIA6`, `CQUAD8` with a
|
||||
//! `PSHELL` that has only a membrane material, every grid holding its out-of-plane degrees of
|
||||
//! freedom 3–6 (`GRID` field PS); edge pressures as the consistent `FORCE`s the model's edge
|
||||
//! integration gives. Plane strain and axisymmetric elements are not written: their cards
|
||||
//! (`PPLANE`/`PLPLANE` with `CPLSTN*`/`CQUADX`, `CTRIAX6`, `PAXI`) differ between Nastran
|
||||
//! dialects, so a deck could not be read the same everywhere;
|
||||
//! - centrifugal loads: `RFORCE` about an axis through the origin of the basic system (speed in
|
||||
//! revolutions per time unit).
|
||||
//!
|
||||
//! Field widths, continuations and number forms are in [`card`]. Model content Nastran cannot
|
||||
//! express is reported as [`Error::Unsupported`], every item at once, and nothing is written.
|
||||
@@ -188,10 +196,31 @@ struct MaterialUse {
|
||||
|
||||
/// One property entry, and what its elements' connection entries need besides their nodes.
|
||||
enum Property {
|
||||
Solid { mid: u32, rule: (Option<&'static str>, Option<&'static str>) },
|
||||
Shell { mid: u32, thickness: f64, offset: Option<f64>, mcid: Option<u32> },
|
||||
Beam { mid: u32, profile: BeamProfile, orientation: [f64; 3] },
|
||||
Rod { mid: u32, area: f64 },
|
||||
Solid {
|
||||
mid: u32,
|
||||
rule: (Option<&'static str>, Option<&'static str>),
|
||||
},
|
||||
Shell {
|
||||
mid: u32,
|
||||
thickness: f64,
|
||||
offset: Option<f64>,
|
||||
mcid: Option<u32>,
|
||||
},
|
||||
Beam {
|
||||
mid: u32,
|
||||
profile: BeamProfile,
|
||||
orientation: [f64; 3],
|
||||
},
|
||||
Rod {
|
||||
mid: u32,
|
||||
area: f64,
|
||||
},
|
||||
/// Plane stress: a shell with only a membrane material.
|
||||
Membrane {
|
||||
mid: u32,
|
||||
thickness: f64,
|
||||
mcid: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A constraint set: per node, the prescribed value of each degree of freedom.
|
||||
@@ -299,7 +328,9 @@ impl<'m> Deck<'m> {
|
||||
let u = uses.entry(s.material.as_str()).or_default();
|
||||
match s.kind {
|
||||
SectionKind::Solid {} => u.solid = true,
|
||||
SectionKind::Shell { .. } => u.shell = true,
|
||||
SectionKind::Shell { .. } | SectionKind::PlaneStress { .. } | SectionKind::PlaneStrain { .. } | SectionKind::Axisymmetric {} => {
|
||||
u.shell = true
|
||||
}
|
||||
SectionKind::Beam { .. } | SectionKind::Rod { .. } => u.line = true,
|
||||
}
|
||||
}
|
||||
@@ -398,6 +429,25 @@ impl<'m> Deck<'m> {
|
||||
let Some(mid) = ids.isotropic else { continue };
|
||||
self.add_property(sname, Property::Rod { mid, area }, elements.iter().copied())?;
|
||||
}
|
||||
SectionKind::PlaneStress { thickness } => {
|
||||
if sec.formulation.integration == Integration::Reduced || sec.formulation.incompatible_modes {
|
||||
self.problems.insert(format!(
|
||||
"section `{sname}`: reduced integration or incompatible modes of plane stress elements (Nastran's membranes have one formulation)"
|
||||
));
|
||||
}
|
||||
let (mid, mcid) = match ids {
|
||||
MaterialIds { isotropic: Some(m), .. } => (m, None),
|
||||
MaterialIds { shell: Some(m), .. } => (m, Some(*self.material_csys.get_or_insert(1))),
|
||||
_ => continue,
|
||||
};
|
||||
self.add_property(sname, Property::Membrane { mid, thickness, mcid }, elements.iter().copied())?;
|
||||
}
|
||||
SectionKind::PlaneStrain { .. } | SectionKind::Axisymmetric {} => {
|
||||
self.problems.insert(format!(
|
||||
"section `{sname}`: {} elements (Nastran's plane strain and axisymmetric cards differ between dialects; not written)",
|
||||
sec.kind.name()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -529,6 +579,22 @@ impl<'m> Deck<'m> {
|
||||
self.problems.insert(format!("load `{name}`: gravity on part of the model (set `{s}`); Nastran's gravity acts on all of it"));
|
||||
}
|
||||
Load::Gravity { .. } => gravity += 1,
|
||||
Load::Centrifugal { set: Some(s), .. } if r.sets.get(s).map(|s| s.elements.len()) != Some(model.elements().len()) => {
|
||||
self.problems
|
||||
.insert(format!("load `{name}`: a rotation of part of the model (set `{s}`); Nastran's RFORCE acts on all of it"));
|
||||
}
|
||||
Load::Centrifugal { point, axis, .. } => {
|
||||
let c = [
|
||||
point[1] * axis[2] - point[2] * axis[1],
|
||||
point[2] * axis[0] - point[0] * axis[2],
|
||||
point[0] * axis[1] - point[1] * axis[0],
|
||||
];
|
||||
if c.iter().any(|v| *v != 0.0) {
|
||||
self.problems.insert(format!(
|
||||
"load `{name}`: a rotation about an axis that misses the origin (written as RFORCE about the origin)"
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let sid = next_id(&mut self.next_sid)?;
|
||||
@@ -597,7 +663,10 @@ impl<'m> Deck<'m> {
|
||||
fn emit_mesh(&self, out: &mut String) -> Result<()> {
|
||||
comment(out, "nodes");
|
||||
for (label, node) in self.model.nodes() {
|
||||
Card::new("GRID").int(*label).blank().reals(node.xyz).write(out)?;
|
||||
let grid = Card::new("GRID").int(*label).blank().reals(node.xyz);
|
||||
// A 2D model's grids hold their out-of-plane translation and rotations.
|
||||
let grid = if self.r.is_planar() { grid.blank().int(3456) } else { grid };
|
||||
grid.write(out)?;
|
||||
}
|
||||
if !self.model.reference_points().is_empty() {
|
||||
comment(out, "reference points");
|
||||
@@ -650,6 +719,7 @@ impl<'m> Deck<'m> {
|
||||
Card::new("PBEAML").int(pid).int(*mid).blank().text(shape).end_line().reals(dims)
|
||||
}
|
||||
Property::Rod { mid, area } => Card::new("PROD").int(pid).int(*mid).real(*area),
|
||||
Property::Membrane { mid, thickness, .. } => Card::new("PSHELL").int(pid).int(*mid).real(*thickness),
|
||||
};
|
||||
write_named(out, &format!("section {section}"), pid, card)?;
|
||||
}
|
||||
@@ -664,6 +734,8 @@ impl<'m> Deck<'m> {
|
||||
// CQUAD8 has four corner thicknesses before the material axes and the offset.
|
||||
Property::Shell { offset, mcid, .. } if el.kind == ElementKind::Quad8 => card.blank().blank().blank().blank().opt(*mcid).opt(*offset),
|
||||
Property::Shell { offset, mcid, .. } => card.opt(*mcid).opt(*offset),
|
||||
Property::Membrane { mcid, .. } if el.kind == ElementKind::Quad8 => card.blank().blank().blank().blank().opt(*mcid),
|
||||
Property::Membrane { mcid, .. } => card.opt(*mcid),
|
||||
Property::Solid { .. } | Property::Rod { .. } => card,
|
||||
};
|
||||
card.write(out)?;
|
||||
@@ -748,6 +820,18 @@ impl<'m> Deck<'m> {
|
||||
entry(Card::new(card).int(*sid).int(*node).blank().real(1.0).reals(*v), out, &mut first)?;
|
||||
}
|
||||
}
|
||||
Load::Pressure { .. } if self.r.is_planar() => {
|
||||
let forces = crate::edge_pressure_loads(self.model, self.r, [*name])?;
|
||||
let mut by_node: BTreeMap<u32, [f64; 3]> = BTreeMap::new();
|
||||
for ((node, dof), v) in forces {
|
||||
if let Some(c) = by_node.entry(node).or_default().get_mut(usize::from(dof) - 1) {
|
||||
*c = v;
|
||||
}
|
||||
}
|
||||
for (node, v) in by_node {
|
||||
entry(Card::new("FORCE").int(*sid).int(node).blank().real(1.0).reals(v), out, &mut first)?;
|
||||
}
|
||||
}
|
||||
Load::Pressure { set, magnitude } => {
|
||||
for f in self.r.sets.get(set).map(|s| s.faces.as_slice()).unwrap_or_default() {
|
||||
if let Some(card) = self.pressure(*sid, *f, *magnitude) {
|
||||
@@ -756,6 +840,12 @@ impl<'m> Deck<'m> {
|
||||
}
|
||||
}
|
||||
Load::Gravity { acceleration, .. } => entry(Card::new("GRAV").int(*sid).blank().real(1.0).reals(*acceleration), out, &mut first)?,
|
||||
Load::Centrifugal { angular_velocity, axis, .. } => {
|
||||
// RFORCE SID G CID A R1 R2 R3: about the basic origin (G blank), A in
|
||||
// revolutions per time unit along the direction R.
|
||||
let a = angular_velocity / (2.0 * std::f64::consts::PI);
|
||||
entry(Card::new("RFORCE").int(*sid).blank().blank().real(a).reals(*axis), out, &mut first)?
|
||||
}
|
||||
Load::BodyForce { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user