forked from 1b-agent/fieldcraft-upstream
LS-DYNA keyword writer (implicit static and eigenvalue, M3) (#25)
* decks: LS-DYNA keyword writer (long format, implicit static and eigenvalue) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * decks: LS-DYNA golden files, Jacobian-sign node-order tests, re-reader and refusals Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs: LS-DYNA writer status and deck notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * decks: simplify the LS-DYNA writer (plan decides once, cards written in place, shared test tables) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * decks: LS-DYNA joins FORMATS (deck.write lsdyna), refusals reported together, shared concentrated loads Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * decks: LS-DYNA on main's shared deck API (steps parameter, model helpers) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 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:
13 files changed
+2353
-3
No files matched your search
@@ -0,0 +1,191 @@
|
||||
//! Element connectivity in LS-DYNA's node order, and the element formulation (ELFORM) for each
|
||||
//! element kind and section formulation.
|
||||
//!
|
||||
//! Node order (from the element figures of the public keyword manual):
|
||||
//! - hexahedra: nodes 1–4 on one face, 5–8 over them, as in the model; the 20-node hexahedron's
|
||||
//! mid-side nodes are on edges 1-2, 2-3, 3-4, 4-1, then 5-6 … 8-5, then 1-5 … 4-8, also as in
|
||||
//! the model;
|
||||
//! - tetrahedra are degenerate hexahedra `n1 n2 n3 n4 n4 n4 n4 n4`;
|
||||
//! - wedges are degenerate hexahedra `n1 n2 n3 n4 n5 n5 n6 n6` whose nodes 1–4 are a
|
||||
//! quadrilateral face, 5 is over edge 1-2 and 6 over edge 3-4. The model's wedge (triangles
|
||||
//! a1-a2-a3 and a4-a5-a6) becomes `a2 a1 a4 a5 a3 a3 a6 a6`: the side a2-a1-a4-a5, then the
|
||||
//! ridge a3–a6;
|
||||
//! - the 10-node tetrahedron has its mid-side nodes 5, 6, 7 on edges 1-2, 2-3, 3-1, and 8, 9, 10
|
||||
//! on edges 2-4, 3-4, 1-4 (the model puts 8, 9, 10 on 1-4, 2-4, 3-4);
|
||||
//! - shells: counter-clockwise corners (the top surface), triangles as `n1 n2 n3 n3` and six-node
|
||||
//! triangles as `n1 n2 n3 n3 n4 n5 n6 n6`; eight-node quadrilaterals as in the model.
|
||||
//!
|
||||
//! Every mapping is checked by the sign of the element Jacobian in the tests.
|
||||
|
||||
use modelcraft_model::{ElementKind, Formulation, Integration, SectionKind};
|
||||
|
||||
/// How an element is written: its keyword and its node list in LS-DYNA order.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(crate) enum Keyword {
|
||||
/// `*ELEMENT_SOLID`: up to 10 nodes on the second card.
|
||||
Solid,
|
||||
/// `*ELEMENT_SOLID_H20`: 20 nodes on two cards.
|
||||
SolidH20,
|
||||
/// `*ELEMENT_SHELL`: 4 or 8 nodes.
|
||||
Shell,
|
||||
/// `*ELEMENT_BEAM`: 2 nodes.
|
||||
Beam,
|
||||
}
|
||||
|
||||
impl Keyword {
|
||||
pub(crate) fn of(kind: ElementKind) -> Keyword {
|
||||
match kind {
|
||||
ElementKind::Hex20 => Keyword::SolidH20,
|
||||
ElementKind::Tet4 | ElementKind::Tet10 | ElementKind::Wedge6 | ElementKind::Hex8 => Keyword::Solid,
|
||||
ElementKind::Tri3 | ElementKind::Tri6 | ElementKind::Quad4 | ElementKind::Quad8 => Keyword::Shell,
|
||||
ElementKind::Rod2 | ElementKind::Beam2 => Keyword::Beam,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn name(self) -> &'static str {
|
||||
match self {
|
||||
Keyword::Solid => "*ELEMENT_SOLID",
|
||||
Keyword::SolidH20 => "*ELEMENT_SOLID_H20",
|
||||
Keyword::Shell => "*ELEMENT_SHELL",
|
||||
Keyword::Beam => "*ELEMENT_BEAM",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Positions in the model's node list, in LS-DYNA's order (repeats for degenerate forms).
|
||||
pub(crate) fn order(kind: ElementKind) -> &'static [usize] {
|
||||
match kind {
|
||||
ElementKind::Rod2 | ElementKind::Beam2 => &[0, 1],
|
||||
ElementKind::Tri3 => &[0, 1, 2, 2],
|
||||
ElementKind::Tri6 => &[0, 1, 2, 2, 3, 4, 5, 5],
|
||||
ElementKind::Quad4 => &[0, 1, 2, 3],
|
||||
ElementKind::Quad8 => &[0, 1, 2, 3, 4, 5, 6, 7],
|
||||
ElementKind::Tet4 => &[0, 1, 2, 3, 3, 3, 3, 3],
|
||||
ElementKind::Tet10 => &[0, 1, 2, 3, 4, 5, 6, 8, 9, 7],
|
||||
ElementKind::Wedge6 => &[1, 0, 3, 4, 2, 2, 5, 5],
|
||||
ElementKind::Hex8 => &[0, 1, 2, 3, 4, 5, 6, 7],
|
||||
ElementKind::Hex20 => &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
|
||||
}
|
||||
}
|
||||
|
||||
/// The element's node labels in LS-DYNA's order.
|
||||
pub(crate) fn connectivity(kind: ElementKind, nodes: &[u32]) -> Option<Vec<u32>> {
|
||||
order(kind).iter().map(|&i| nodes.get(i).copied()).collect()
|
||||
}
|
||||
|
||||
/// Hourglass control a reduced-integration formulation needs (`*HOURGLASS`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(crate) enum Hourglass {
|
||||
/// Solids: the assumed-strain stiffness form (type 6) with coefficient 1, which gives
|
||||
/// accurate coarse-mesh bending for elastic materials.
|
||||
Solid,
|
||||
/// Quadrilateral shells: the stiffness form (type 4) with the default coefficient.
|
||||
Shell,
|
||||
}
|
||||
|
||||
/// A part's element formulation.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) struct Elform {
|
||||
pub(crate) elform: i32,
|
||||
pub(crate) hourglass: Option<Hourglass>,
|
||||
}
|
||||
|
||||
/// The formulation of a section's elements of one kind.
|
||||
///
|
||||
/// Solids: hex8 2 (eight-point), reduced 1 (one-point) with hourglass control, incompatible
|
||||
/// modes −18 (eight-point enhanced strain with incompatible modes); wedges 2 (as degenerate
|
||||
/// 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).
|
||||
/// 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 });
|
||||
let reduced = f.integration == Integration::Reduced;
|
||||
let no = |what: &str| Err(format!("section `{section_name}`: {what} {}", kind.name()));
|
||||
match section {
|
||||
SectionKind::Solid {} => {
|
||||
if f.hybrid {
|
||||
return match kind {
|
||||
ElementKind::Tet4 => plain(13),
|
||||
_ => no(
|
||||
"the hybrid (mixed displacement-pressure) formulation has a counterpart only for 4-node tetrahedra (nodal pressure, ELFORM 13), not",
|
||||
),
|
||||
};
|
||||
}
|
||||
match kind {
|
||||
ElementKind::Hex8 if f.incompatible_modes => plain(-18),
|
||||
ElementKind::Hex8 if reduced => Ok(Elform { elform: 1, hourglass: Some(Hourglass::Solid) }),
|
||||
ElementKind::Hex8 => plain(2),
|
||||
ElementKind::Wedge6 if reduced => plain(15),
|
||||
ElementKind::Wedge6 => plain(2),
|
||||
ElementKind::Tet4 => plain(10),
|
||||
_ if reduced && kind.is_quadratic() => no("there is no reduced-integration form of"),
|
||||
ElementKind::Tet10 => plain(16),
|
||||
ElementKind::Hex20 => plain(23),
|
||||
_ => no("a solid section cannot hold"),
|
||||
}
|
||||
}
|
||||
SectionKind::Shell { .. } => match kind {
|
||||
ElementKind::Quad4 if reduced => Ok(Elform { elform: 2, hourglass: Some(Hourglass::Shell) }),
|
||||
ElementKind::Quad4 => plain(16),
|
||||
ElementKind::Tri3 => plain(4),
|
||||
_ if reduced && kind.is_quadratic() => {
|
||||
no("reduced integration of quadratic shells is a whole-model setting in LS-DYNA, not written per section, for")
|
||||
}
|
||||
ElementKind::Quad8 => plain(23),
|
||||
ElementKind::Tri6 => plain(24),
|
||||
_ => no("a shell section cannot hold"),
|
||||
},
|
||||
SectionKind::Rod { .. } => match kind {
|
||||
ElementKind::Rod2 => plain(3),
|
||||
_ => no("a rod section cannot hold"),
|
||||
},
|
||||
SectionKind::Beam { .. } => no("beam sections are not written to LS-DYNA decks yet (planned with beam profiles):"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_kind_maps_every_node_once_except_degenerate_repeats() {
|
||||
for kind in ElementKind::ALL {
|
||||
let o = order(kind);
|
||||
let mut seen: Vec<usize> = o.to_vec();
|
||||
seen.sort_unstable();
|
||||
seen.dedup();
|
||||
assert_eq!(seen, (0..kind.node_count()).collect::<Vec<_>>(), "{}", kind.name());
|
||||
let nodes: Vec<u32> = (1..=kind.node_count() as u32).collect();
|
||||
assert_eq!(connectivity(kind, &nodes).unwrap().len(), o.len());
|
||||
assert!(connectivity(kind, &nodes[..1]).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formulations_map_or_say_why_not() {
|
||||
let solid = SectionKind::Solid {};
|
||||
let full = Formulation::default();
|
||||
let reduced = Formulation { integration: Integration::Reduced, ..full };
|
||||
let im = Formulation { incompatible_modes: true, ..full };
|
||||
let hybrid = Formulation { hybrid: true, ..full };
|
||||
assert_eq!(elform(ElementKind::Hex8, &solid, full, "s").unwrap().elform, 2);
|
||||
assert_eq!(elform(ElementKind::Hex8, &solid, reduced, "s").unwrap(), Elform { elform: 1, hourglass: Some(Hourglass::Solid) });
|
||||
assert_eq!(elform(ElementKind::Hex8, &solid, im, "s").unwrap().elform, -18);
|
||||
assert_eq!(elform(ElementKind::Tet4, &solid, hybrid, "s").unwrap().elform, 13);
|
||||
assert_eq!(elform(ElementKind::Tet10, &solid, full, "s").unwrap().elform, 16);
|
||||
assert_eq!(elform(ElementKind::Hex20, &solid, full, "s").unwrap().elform, 23);
|
||||
for (k, f) in [(ElementKind::Hex8, hybrid), (ElementKind::Tet10, reduced), (ElementKind::Hex20, reduced)] {
|
||||
let e = elform(k, &solid, f, "web").unwrap_err();
|
||||
assert!(e.contains("section `web`") && e.contains(k.name()), "{e}");
|
||||
}
|
||||
let shell = SectionKind::Shell { thickness: 1.0, offset: 0.0, points: 5 };
|
||||
assert_eq!(elform(ElementKind::Quad4, &shell, full, "s").unwrap().elform, 16);
|
||||
assert_eq!(elform(ElementKind::Quad4, &shell, reduced, "s").unwrap().hourglass, Some(Hourglass::Shell));
|
||||
assert_eq!(elform(ElementKind::Tri6, &shell, full, "s").unwrap().elform, 24);
|
||||
assert!(elform(ElementKind::Quad8, &shell, reduced, "s").is_err());
|
||||
assert!(elform(ElementKind::Hex8, &shell, full, "s").is_err());
|
||||
assert_eq!(elform(ElementKind::Rod2, &SectionKind::Rod { area: 1.0 }, full, "s").unwrap().elform, 3);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user