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:
authored and GitHub committed 2026-10-13 17:39:45 -04:00
1 parent f4a0333d28
commit 81fa8d7220
13 files changed
+2353 -3

No files matched your search

+676
View File
@@ -0,0 +1,676 @@
//! LS-DYNA deck writer: golden files of our own small models, node-order mappings checked by the
//! sign of the element Jacobian, a re-read of our own output, and what the format cannot take.
//!
//! Golden files live in `tests/lsdyna/`; `MODELCRAFT_BLESS=1 cargo test -p modelcraft-decks`
//! rewrites them after an intended change (review the diff).
use std::collections::{BTreeMap, BTreeSet};
use modelcraft_decks::{Error, lsdyna};
use modelcraft_model::*;
// ---------------------------------------------------------------------------------------------
// Our own models.
fn steel() -> Material {
Material { elastic: Some(Elastic::Isotropic { young: 210000.0, poisson: 0.3 }), density: Some(7.85e-9) }
}
fn step(name: &str, procedure: Procedure, bcs: &[&str], loads: &[&str]) -> Step {
let mut s = Step::new(name, procedure);
s.boundary_conditions = bcs.iter().map(|b| b.to_string()).collect();
s.loads = loads.iter().map(|l| l.to_string()).collect();
s
}
/// A 4 × 1 × 1 cantilever of four hex8 elements along x, fixed at x = 0. Nodes are numbered
/// x-fastest: label = 1 + i + 5·(j + 2·k).
fn cantilever_mesh(m: &mut Model) {
m.set_units(UnitSystem::MmTonneS);
let label = |i: u32, j: u32, k: u32| 1 + i + 5 * (j + 2 * k);
for k in 0..2 {
for j in 0..2 {
for i in 0..5 {
m.set_node(label(i, j, k), [f64::from(i), f64::from(j), f64::from(k)]).unwrap();
}
}
}
for i in 0..4 {
let nodes = vec![
label(i, 0, 0),
label(i + 1, 0, 0),
label(i + 1, 1, 0),
label(i, 1, 0),
label(i, 0, 1),
label(i + 1, 0, 1),
label(i + 1, 1, 1),
label(i, 1, 1),
];
m.set_element(i + 1, ElementKind::Hex8, nodes).unwrap();
}
m.add_set("all", Set { elements: vec![1, 2, 3, 4], ..Set::default() }).unwrap();
m.add_set("root", Set { nodes: vec![label(0, 0, 0), label(0, 1, 0), label(0, 0, 1), label(0, 1, 1)], ..Set::default() }).unwrap();
// Face 4 of a hex8 is the side along edge 2-3: x = 4 for the last element.
m.add_set("tip", Set { faces: vec![ElementFace { element: 4, face: 4 }], ..Set::default() }).unwrap();
m.add_boundary_condition("clamp", BoundaryCondition::Fixed { target: Target::Set("root".into()) }).unwrap();
}
fn cantilever() -> Model {
let mut m = Model::default();
cantilever_mesh(&mut m);
m.add_material("Steel", steel()).unwrap();
m.add_section("beam", Section { set: "all".into(), material: "Steel".into(), kind: SectionKind::Solid {}, formulation: Formulation::default() })
.unwrap();
m.add_load("tip_load", Load::Force { target: Target::Set("tip".into()), vector: [0.0, 0.0, -100.0], distribution: Distribution::Total }).unwrap();
m.add_step(step("Bend", Procedure::Static {}, &["clamp"], &["tip_load"]), None).unwrap();
m
}
/// The cantilever in an orthotropic material with incompatible modes, loaded through a coupled
/// reference point, then its natural frequencies: two steps, so two cases.
fn cantilever_modes() -> Model {
let mut m = Model::default();
cantilever_mesh(&mut m);
let ortho = Elastic::EngineeringConstants { young: [140000.0, 10000.0, 10000.0], poisson: [0.3, 0.3, 0.45], shear: [5000.0, 5000.0, 3500.0] };
m.add_material("Ply", Material { elastic: Some(ortho), density: Some(1.6e-9) }).unwrap();
let f = Formulation { incompatible_modes: true, ..Formulation::default() };
m.add_section("beam", Section { set: "all".into(), material: "Ply".into(), kind: SectionKind::Solid {}, formulation: f }).unwrap();
m.add_reference_point("RP", ReferencePoint { xyz: [5.0, 0.5, 0.5], label: 0 }).unwrap();
m.add_coupling("pad", Coupling { reference_point: "RP".into(), set: "tip".into(), kind: CouplingKind::Kinematic, dofs: Dofs::all() }).unwrap();
m.add_load("twist", Load::Moment { target: Target::ReferencePoint("RP".into()), vector: [250.0, 0.0, 0.0], distribution: Distribution::Total })
.unwrap();
m.add_load("push", Load::Force { target: Target::ReferencePoint("RP".into()), vector: [0.0, -10.0, 0.0], distribution: Distribution::Total })
.unwrap();
m.add_step(step("Bend", Procedure::Static {}, &["clamp"], &["push", "twist"]), None).unwrap();
m.add_step(step("Modes", Procedure::Frequency { modes: 6, min_hz: Some(1.0), max_hz: Some(5000.0) }, &["clamp"], &[]), None).unwrap();
m
}
/// A 2 × 1 plate at z = 0: two quad4 and two tri3 shells, held on the edge x = 0, pressed and
/// under gravity.
fn plate() -> Model {
let mut m = Model::default();
m.set_units(UnitSystem::MmTonneS);
for (l, x, y) in
[(1, 0.0, 0.0), (2, 10.0, 0.0), (3, 20.0, 0.0), (4, 0.0, 10.0), (5, 10.0, 10.0), (6, 20.0, 10.0), (7, 30.0, 0.0), (8, 30.0, 10.0)]
{
m.set_node(l, [x, y, 0.0]).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();
m.set_element(3, ElementKind::Tri3, vec![3, 7, 8]).unwrap();
m.set_element(4, ElementKind::Tri3, vec![3, 8, 6]).unwrap();
m.add_set("plate", Set { elements: vec![1, 2, 3, 4], ..Set::default() }).unwrap();
m.add_set("edge", Set { nodes: vec![1, 4], ..Set::default() }).unwrap();
let top: Vec<ElementFace> = (1..=4).map(|element| ElementFace { element, face: 1 }).collect();
m.add_set("top", Set { faces: top, ..Set::default() }).unwrap();
m.add_material("Steel", steel()).unwrap();
m.add_section(
"skin",
Section {
set: "plate".into(),
material: "Steel".into(),
kind: SectionKind::Shell { thickness: 0.5, offset: 0.25, points: 5 },
formulation: Formulation::default(),
},
)
.unwrap();
m.add_boundary_condition("hold", BoundaryCondition::Fixed { target: Target::Set("edge".into()) }).unwrap();
m.add_load("press", Load::Pressure { set: "top".into(), magnitude: 0.01 }).unwrap();
m.add_load("weight", Load::Gravity { acceleration: [0.0, 0.0, -9810.0], set: None }).unwrap();
m.add_step(step("Sag", Procedure::Static {}, &["hold"], &["press", "weight"]), None).unwrap();
m
}
/// One element of each solid kind besides hex8, side by side (each its own section), with a
/// distributing coupling, a prescribed displacement and a traction.
fn solids() -> Model {
let mut m = Model::default();
m.set_units(UnitSystem::MKgS);
let mut next = 0u32;
let mut node = |m: &mut Model, xyz: [f64; 3]| {
next += 1;
m.set_node(next, xyz).unwrap();
next
};
// tet4
let t: Vec<u32> = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]].iter().map(|p| node(&mut m, *p)).collect();
m.set_element(1, ElementKind::Tet4, t.clone()).unwrap();
// tet10: corners then mid-sides on 1-2, 2-3, 3-1, 1-4, 2-4, 3-4.
let c = [[2.0, 0.0, 0.0], [3.0, 0.0, 0.0], [2.0, 1.0, 0.0], [2.0, 0.0, 1.0]];
let mid = |a: usize, b: usize| [(c[a][0] + c[b][0]) / 2.0, (c[a][1] + c[b][1]) / 2.0, (c[a][2] + c[b][2]) / 2.0];
let mut t10: Vec<u32> = c.iter().map(|p| node(&mut m, *p)).collect();
for (a, b) in [(0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)] {
t10.push(node(&mut m, mid(a, b)));
}
m.set_element(2, ElementKind::Tet10, t10).unwrap();
// wedge6
let w: Vec<u32> = [[4.0, 0.0, 0.0], [5.0, 0.0, 0.0], [4.0, 1.0, 0.0], [4.0, 0.0, 1.0], [5.0, 0.0, 1.0], [4.0, 1.0, 1.0]]
.iter()
.map(|p| node(&mut m, *p))
.collect();
m.set_element(3, ElementKind::Wedge6, w).unwrap();
// hex20
let h = hex20_nodes([6.0, 0.0, 0.0]);
let h: Vec<u32> = h.iter().map(|p| node(&mut m, *p)).collect();
m.set_element(4, ElementKind::Hex20, h).unwrap();
m.add_material("Aluminium", Material { elastic: Some(Elastic::Isotropic { young: 70e9, poisson: 0.33 }), density: Some(2700.0) }).unwrap();
for (e, name) in [(1, "tet4"), (2, "tet10"), (3, "wedge"), (4, "hex20")] {
m.add_set(name, Set { elements: vec![e], ..Set::default() }).unwrap();
let f = Formulation { hybrid: e == 1, ..Formulation::default() };
m.add_section(name, Section { set: name.into(), material: "Aluminium".into(), kind: SectionKind::Solid {}, formulation: f }).unwrap();
}
m.add_set("base", Set { nodes: t.clone(), ..Set::default() }).unwrap();
// Face 2 of the hex20 is its top (nodes 5-8 and their mid-sides).
m.add_set("lid", Set { faces: vec![ElementFace { element: 4, face: 2 }], ..Set::default() }).unwrap();
m.add_reference_point("RP", ReferencePoint { xyz: [6.5, 0.5, 2.0], label: 0 }).unwrap();
m.add_coupling("spider", Coupling { reference_point: "RP".into(), set: "lid".into(), kind: CouplingKind::Distributing, dofs: Dofs::all() })
.unwrap();
m.add_boundary_condition("hold", BoundaryCondition::Fixed { target: Target::Set("base".into()) }).unwrap();
m.add_boundary_condition(
"lift",
BoundaryCondition::Displacement { target: Target::Set("tet10".into()), values: [None, None, Some(0.001), None, None, None] },
)
.unwrap();
m.add_load("drag", Load::Traction { set: "lid".into(), vector: [1000.0, 0.0, 0.0] }).unwrap();
m.add_load("pull", Load::Force { target: Target::ReferencePoint("RP".into()), vector: [0.0, 0.0, 50.0], distribution: Distribution::Total })
.unwrap();
m.add_step(step("Load", Procedure::Static {}, &["hold", "lift"], &["drag", "pull"]), None).unwrap();
m
}
/// A unit 20-node hexahedron at `o` in model order.
fn hex20_nodes(o: [f64; 3]) -> Vec<[f64; 3]> {
let corners = CUBE.map(|c| c.map(|v| (v + 1.0) / 2.0));
let mut out: Vec<[f64; 3]> = corners.to_vec();
out.extend(HEX_EDGES.map(|(a, b)| [0, 1, 2].map(|k| (corners[a][k] + corners[b][k]) / 2.0)));
out.iter().map(|p| [p[0] + o[0], p[1] + o[1], p[2] + o[2]]).collect()
}
// ---------------------------------------------------------------------------------------------
// Golden files.
fn golden(name: &str, model: &Model) -> lsdyna::Written {
let written = lsdyna::write(model).unwrap();
let again = lsdyna::write(model).unwrap();
assert_eq!(written, again, "deterministic");
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/lsdyna").join(format!("{name}.k"));
if std::env::var_os("MODELCRAFT_BLESS").is_some() {
std::fs::write(&path, &written.text).unwrap();
}
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (run with MODELCRAFT_BLESS=1)", path.display()));
assert!(written.text == expected, "{name}.k differs from the golden file (MODELCRAFT_BLESS=1 rewrites it)");
assert!(written.text.is_ascii());
for line in written.text.lines() {
assert!(line.len() <= 200 && line == line.trim_end(), "{line:?}");
assert!(!line.contains('\t'));
}
assert_eq!(written.rounded_values, 0);
written
}
#[test]
fn golden_hex8_cantilever() {
let w = golden("cantilever", &cantilever());
assert_eq!(w.cases, 0);
let deck = read(&w.text);
assert_eq!(deck.nodes.len(), 20);
// 100 N on the tip face: 25 N on each of its four nodes, written once on the node set.
assert!(w.text.contains("*LOAD_NODE_SET"));
assert_eq!(deck.node_sets["tip"], vec![5, 10, 15, 20]);
assert_eq!(deck.node_sets["root"], vec![1, 6, 11, 16]);
}
#[test]
fn golden_cantilever_modes_in_cases() {
let w = golden("cantilever_modes", &cantilever_modes());
assert_eq!(w.cases, 2);
for k in ["*CASE_BEGIN_1", "*CASE_END_1", "*CASE_BEGIN_2", "*CASE_END_2", "*CONTROL_IMPLICIT_EIGENVALUE", "*MAT_ORTHOTROPIC_ELASTIC_TITLE"] {
assert!(w.text.contains(k), "{k}");
}
// The rigid body's set leads with the reference point.
let deck = read(&w.text);
let rp = cantilever_modes().reference_point("RP").unwrap().label;
assert_eq!(deck.node_sets["pad (rigid body)"].first(), Some(&rp));
assert_eq!(deck.nodes[&rp], [5.0, 0.5, 0.5]);
}
#[test]
fn golden_plate_with_pressure_and_gravity() {
let w = golden("plate", &plate());
assert!(w.text.contains("*LOAD_SEGMENT_SET") && w.text.contains("*LOAD_BODY_Z"));
let deck = read(&w.text);
// Triangles are degenerate quadrilaterals.
assert_eq!(deck.elements[&3], vec![3, 7, 8, 8]);
assert_eq!(deck.parts.len(), 2, "quad4 and tri3 take different formulations");
}
#[test]
fn golden_solids_and_distributing_coupling() {
let w = golden("solids", &solids());
for k in ["*ELEMENT_SOLID_H20", "*CONSTRAINED_INTERPOLATION", "*BOUNDARY_PRESCRIBED_MOTION_SET_ID", "*LOAD_NODE_POINT"] {
assert!(w.text.contains(k), "{k}");
}
}
// ---------------------------------------------------------------------------------------------
// A minimal reader of our own output: nodes, elements and node sets.
#[derive(Default)]
struct Read {
nodes: BTreeMap<u32, [f64; 3]>,
/// Element → its nodes as written.
elements: BTreeMap<u32, Vec<u32>>,
/// The part ids elements are in.
parts: BTreeSet<u32>,
node_sets: BTreeMap<String, Vec<u32>>,
}
fn fields(line: &str) -> Vec<&str> {
(0..line.len()).step_by(20).map(|i| line[i..(i + 20).min(line.len())].trim()).collect()
}
fn read(text: &str) -> Read {
let mut out = Read::default();
let mut keyword = String::new();
let mut cards: Vec<&str> = Vec::new();
let flush = |keyword: &str, cards: &[&str], out: &mut Read| match keyword {
"*NODE" => {
for c in cards {
let f = fields(c);
out.nodes.insert(f[0].parse().unwrap(), [f[1].parse().unwrap(), f[2].parse().unwrap(), f[3].parse().unwrap()]);
}
}
"*ELEMENT_SOLID" | "*ELEMENT_SOLID_H20" => {
let per = if keyword == "*ELEMENT_SOLID" { 2 } else { 3 };
for e in cards.chunks(per) {
let head = fields(e[0]);
let nodes: Vec<u32> = e[1..].iter().flat_map(|c| fields(c)).map(|v| v.parse().unwrap()).collect();
out.elements.insert(head[0].parse().unwrap(), nodes);
out.parts.insert(head[1].parse().unwrap());
}
}
"*ELEMENT_SHELL" | "*ELEMENT_BEAM" => {
for c in cards {
let f: Vec<u32> = fields(c).iter().map(|v| v.parse().unwrap()).collect();
out.elements.insert(f[0], f[2..].to_vec());
out.parts.insert(f[1]);
}
}
"*SET_NODE_LIST_TITLE" => {
let nodes = cards[2..].iter().flat_map(|c| fields(c)).map(|v| v.parse().unwrap()).collect();
out.node_sets.insert(cards[0].to_string(), nodes);
}
_ => {}
};
for line in text.lines() {
if line.starts_with('$') {
continue;
}
if line.starts_with('*') {
flush(&keyword, &cards, &mut out);
keyword = line.to_string();
cards.clear();
} else {
cards.push(line);
}
}
flush(&keyword, &cards, &mut out);
out
}
#[test]
fn re_read_gives_back_nodes_elements_and_sets() {
for model in [cantilever(), cantilever_modes(), plate(), solids()] {
let w = lsdyna::write(&model).unwrap();
let deck = read(&w.text);
for (label, n) in model.nodes() {
assert_eq!(deck.nodes[label], n.xyz, "node {label} round-trips exactly");
}
for (label, e) in model.elements() {
let written = &deck.elements[label];
let unique: BTreeSet<u32> = written.iter().copied().collect();
assert_eq!(unique, e.nodes.iter().copied().collect::<BTreeSet<u32>>(), "element {label}");
}
let r = model.resolve().unwrap();
for (name, nodes) in &deck.node_sets {
if let Some(s) = r.sets.get(name) {
assert_eq!(nodes, &s.nodes, "set {name}");
}
}
}
}
#[test]
fn awkward_coordinates_round_trip_or_are_counted() {
let mut m = cantilever();
// Full 17-digit mantissas at every magnitude a model allows.
let values = [0.1 + 0.2, -123_456_789.123_456_78, 1e-300, -2.2250738585072014e-308, 0.012_345_678_901_234_567, 999_999_999.999_999_9];
for (i, v) in values.iter().enumerate() {
m.set_node(100 + i as u32, [*v, -*v, *v / 3.0]).unwrap();
}
let w = lsdyna::write(&m).unwrap();
let deck = read(&w.text);
let mut inexact = 0;
for (i, _) in values.iter().enumerate() {
let label = 100 + i as u32;
let want = m.node(label).unwrap().xyz;
let got = deck.nodes[&label];
for (a, b) in want.iter().zip(got) {
if *a != b {
inexact += 1;
assert!(((a - b) / a).abs() < 5e-13, "{a:e} {b:e}");
}
}
}
assert_eq!(inexact, w.rounded_values);
}
// ---------------------------------------------------------------------------------------------
// Node order: the sign of the Jacobian in LS-DYNA's element conventions.
/// Hexahedron corners in the reference cube, in LS-DYNA (and model) order.
const CUBE: [[f64; 3]; 8] =
[[-1., -1., -1.], [1., -1., -1.], [1., 1., -1.], [-1., 1., -1.], [-1., -1., 1.], [1., -1., 1.], [1., 1., 1.], [-1., 1., 1.]];
/// Hexahedron edges as corner pairs, in mid-side node order.
const HEX_EDGES: [(usize, usize); 12] = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)];
/// det J of the map with shape-function gradients `dn[i]` (reference axes) at nodes `x[i]`.
fn jacobian_det(x: &[[f64; 3]], dn: impl Iterator<Item = [f64; 3]>) -> f64 {
let mut j = [[0.0; 3]; 3];
for (p, d) in x.iter().zip(dn) {
for a in 0..3 {
for b in 0..3 {
j[a][b] += d[a] * p[b];
}
}
}
det(j)
}
/// Trilinear hexahedron in LS-DYNA order (1-4 one face, 5-8 over them): det J at a point.
fn hex8_det(x: &[[f64; 3]], (r, s, t): (f64, f64, f64)) -> f64 {
jacobian_det(
x,
CUBE.iter().map(|c| {
[
c[0] * (1.0 + c[1] * s) * (1.0 + c[2] * t) / 8.0,
c[1] * (1.0 + c[0] * r) * (1.0 + c[2] * t) / 8.0,
c[2] * (1.0 + c[0] * r) * (1.0 + c[1] * s) / 8.0,
]
}),
)
}
fn det(j: [[f64; 3]; 3]) -> f64 {
j[0][0] * (j[1][1] * j[2][2] - j[1][2] * j[2][1]) - j[0][1] * (j[1][0] * j[2][2] - j[1][2] * j[2][0])
+ j[0][2] * (j[1][0] * j[2][1] - j[1][1] * j[2][0])
}
const GAUSS: f64 = 0.577_350_269_189_625_8;
/// Write a one-element model of `kind` with these model-order coordinates and read back the
/// written coordinates in LS-DYNA order.
fn written_coordinates(kind: ElementKind, xyz: &[[f64; 3]]) -> Vec<[f64; 3]> {
let mut m = Model::default();
for (i, p) in xyz.iter().enumerate() {
m.set_node(i as u32 + 1, *p).unwrap();
}
m.set_element(1, kind, (1..=xyz.len() as u32).collect()).unwrap();
m.add_set("all", Set { elements: vec![1], ..Set::default() }).unwrap();
m.add_set("fix", Set { nodes: vec![1], ..Set::default() }).unwrap();
m.add_material("M", steel()).unwrap();
let kind_of = if kind.dimension() == 3 { SectionKind::Solid {} } else { SectionKind::Shell { thickness: 1.0, offset: 0.0, points: 3 } };
m.add_section("s", Section { set: "all".into(), material: "M".into(), kind: kind_of, formulation: Formulation::default() }).unwrap();
m.add_boundary_condition("b", BoundaryCondition::Fixed { target: Target::Set("fix".into()) }).unwrap();
m.add_step(step("S", Procedure::Frequency { modes: 1, min_hz: None, max_hz: None }, &["b"], &[]), None).unwrap();
let deck = read(&lsdyna::write(&m).unwrap().text);
deck.elements[&1].iter().map(|n| deck.nodes[n]).collect()
}
/// Slightly distorted coordinates, so a wrong mapping cannot pass by symmetry (short decimals,
/// so every coordinate is written exactly).
fn jiggle(p: [f64; 3], i: usize) -> [f64; 3] {
let k = ((i as f64 * 0.37).sin() * 0.03 * 1e4).round() / 1e4;
[p[0] + k, p[1] - k * 0.5, p[2] + k * 0.25].map(|c| (c * 1e6).round() / 1e6)
}
#[test]
fn degenerate_solids_have_positive_jacobians_in_lsdyna_order() {
let tet = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let wedge = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0]];
let hex =
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0]];
for (kind, xyz) in [(ElementKind::Tet4, &tet[..]), (ElementKind::Wedge6, &wedge[..]), (ElementKind::Hex8, &hex[..])] {
let xyz: Vec<[f64; 3]> = xyz.iter().enumerate().map(|(i, p)| jiggle(*p, i)).collect();
let x = written_coordinates(kind, &xyz);
assert_eq!(x.len(), 8, "{}", kind.name());
for r in [-GAUSS, GAUSS] {
for s in [-GAUSS, GAUSS] {
for t in [-GAUSS, GAUSS] {
assert!(hex8_det(&x, (r, s, t)) > 0.0, "{} at ({r}, {s}, {t})", kind.name());
}
}
}
// The model's own order mirrored is negative (the test can fail).
let mut mirrored = xyz.clone();
for p in &mut mirrored {
p[2] = -p[2];
}
let x = written_coordinates_unchecked(kind, &mirrored);
assert!(hex8_det(&x, (0.1, -0.1, 0.1)) < 0.0, "{}", kind.name());
}
}
/// [`written_coordinates`] without writing: the writer's node order applied to coordinates the
/// model would reject as inverted.
fn written_coordinates_unchecked(kind: ElementKind, xyz: &[[f64; 3]]) -> Vec<[f64; 3]> {
lsdyna::node_order(kind).iter().map(|&i| xyz[i]).collect()
}
/// Quadratic tetrahedron in LS-DYNA order (mid-sides 5, 6, 7 on 1-2, 2-3, 3-1 and 8, 9, 10 on
/// 2-4, 3-4, 1-4): det J at volume coordinates (l1, l2, l3).
fn tet10_det(x: &[[f64; 3]], l: [f64; 3]) -> f64 {
let ls = [1.0 - l[0] - l[1] - l[2], l[0], l[1], l[2]];
// dL/d(ξ, η, ζ) with L1 = 1 − ξ − η − ζ, L2 = ξ, L3 = η, L4 = ζ.
let dl = [[-1.0, -1.0, -1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let edges = [(0, 1), (1, 2), (2, 0), (1, 3), (2, 3), (0, 3)];
let corners = (0..4).map(|i| dl[i].map(|d| (4.0 * ls[i] - 1.0) * d));
let mids = edges.iter().map(|&(a, b)| [0, 1, 2].map(|c| 4.0 * (dl[a][c] * ls[b] + ls[a] * dl[b][c])));
jacobian_det(x, corners.chain(mids))
}
#[test]
fn tet10_mid_side_nodes_sit_on_lsdyna_edges_with_positive_jacobian() {
let c = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let mut xyz: Vec<[f64; 3]> = c.iter().enumerate().map(|(i, p)| jiggle(*p, i)).collect();
for (a, b) in [(0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)] {
xyz.push([0, 1, 2].map(|k| (xyz[a][k] + xyz[b][k]) / 2.0));
}
let x = written_coordinates(ElementKind::Tet10, &xyz);
assert_eq!(x.len(), 10);
for (m, (a, b)) in [(0, 1), (1, 2), (2, 0), (1, 3), (2, 3), (0, 3)].iter().enumerate() {
let want = [0, 1, 2].map(|k| (x[*a][k] + x[*b][k]) / 2.0);
assert_eq!(x[4 + m], want, "node {} is on LS-DYNA edge {}-{}", 5 + m, a + 1, b + 1);
}
for l in [[0.25, 0.25, 0.25], [0.1, 0.1, 0.1], [0.6, 0.1, 0.1], [0.1, 0.6, 0.1], [0.1, 0.1, 0.6]] {
assert!(tet10_det(&x, l) > 0.0, "{l:?}");
}
}
/// 20-node serendipity hexahedron in LS-DYNA order: det J at a point.
fn hex20_det(x: &[[f64; 3]], p: [f64; 3]) -> f64 {
let mut nodes: Vec<[f64; 3]> = CUBE.to_vec();
nodes.extend(HEX_EDGES.map(|(a, b)| [0, 1, 2].map(|k| (CUBE[a][k] + CUBE[b][k]) / 2.0)));
let gradient = |n: &[f64; 3]| -> [f64; 3] {
let q = [0, 1, 2].map(|k| 1.0 + n[k] * p[k]);
match (0..3).find(|k| n[*k] == 0.0) {
// Corner: N = q0 q1 q2 (n·p − 2) / 8.
None => {
let s = n[0] * p[0] + n[1] * p[1] + n[2] * p[2] - 2.0;
[0, 1, 2].map(|k| n[k] * q[(k + 1) % 3] * q[(k + 2) % 3] * (s + q[k]) / 8.0)
}
// Mid-side on an edge along axis z: N = (1 − p_z²) q_a q_b / 4.
Some(z) => [0, 1, 2].map(|k| {
let others = |skip: usize| (0..3).filter(|m| *m != skip && *m != z).map(|m| q[m]).product::<f64>();
if k == z { -2.0 * p[z] * others(z) / 4.0 } else { n[k] * (1.0 - p[z] * p[z]) * others(k) / 4.0 }
}),
}
};
jacobian_det(x, nodes.iter().map(gradient))
}
#[test]
fn hex20_keeps_the_model_order_with_positive_jacobian() {
let xyz: Vec<[f64; 3]> = hex20_nodes([0.0; 3]).into_iter().enumerate().map(|(i, p)| jiggle(p, i)).collect();
let x = written_coordinates(ElementKind::Hex20, &xyz);
assert_eq!(x, xyz);
for r in [-GAUSS, GAUSS] {
for s in [-GAUSS, GAUSS] {
for t in [-GAUSS, GAUSS] {
assert!(hex20_det(&x, [r, s, t]) > 0.0);
}
}
}
// The unit cube's Jacobian is 1/8 everywhere: the shape functions are right.
assert!((hex20_det(&hex20_nodes([0.0; 3]), [0.3, -0.2, 0.7]) - 0.125).abs() < 1e-12);
}
#[test]
fn shells_keep_their_normal() {
let normal = |x: &[[f64; 3]]| {
// Diagonals of the (possibly degenerate) quadrilateral 1-2-3-4.
let d1 = [0, 1, 2].map(|k| x[2][k] - x[0][k]);
let d2 = [0, 1, 2].map(|k| x[3][k] - x[1][k]);
[d1[1] * d2[2] - d1[2] * d2[1], d1[2] * d2[0] - d1[0] * d2[2], d1[0] * d2[1] - d1[1] * d2[0]]
};
let tri = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let quad = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]];
let mid = |p: &[[f64; 3]], a: usize, b: usize| [0, 1, 2].map(|k| (p[a][k] + p[b][k]) / 2.0);
let tri6: Vec<[f64; 3]> = tri.iter().copied().chain([mid(&tri, 0, 1), mid(&tri, 1, 2), mid(&tri, 2, 0)]).collect();
let quad8: Vec<[f64; 3]> = quad.iter().copied().chain([mid(&quad, 0, 1), mid(&quad, 1, 2), mid(&quad, 2, 3), mid(&quad, 3, 0)]).collect();
for (kind, xyz) in
[(ElementKind::Tri3, tri.to_vec()), (ElementKind::Quad4, quad.to_vec()), (ElementKind::Tri6, tri6), (ElementKind::Quad8, quad8)]
{
let x = written_coordinates(kind, &xyz);
assert!(normal(&x)[2] > 0.0, "{} keeps +z as its top", kind.name());
if kind == ElementKind::Tri6 {
assert_eq!(&x[4..7], &xyz[3..6]);
}
if kind == ElementKind::Quad8 {
assert_eq!(&x[4..8], &xyz[4..8]);
}
}
}
// ---------------------------------------------------------------------------------------------
// What the format cannot take, and hostile models.
fn unsupported(m: &Model, needle: &str) {
match lsdyna::write(m) {
Err(Error::Unsupported { format, what }) => {
assert_eq!(format, "LS-DYNA");
assert!(what.contains(needle), "{what}");
}
other => panic!("expected unsupported `{needle}`, got {other:?}"),
}
}
#[test]
fn what_lsdyna_cannot_take_is_named() {
let mut m = cantilever();
m.add_load("heat", Load::BodyForce { set: "all".into(), vector: [0.0, 0.0, 1.0] }).unwrap();
m.replace_step(step("Bend", Procedure::Static {}, &["clamp"], &["tip_load", "heat"])).unwrap();
unsupported(&m, "load `heat`");
let mut m = cantilever();
m.replace_section(
"beam",
Section {
set: "all".into(),
material: "Steel".into(),
kind: SectionKind::Solid {},
formulation: Formulation { hybrid: true, ..Formulation::default() },
},
)
.unwrap();
unsupported(&m, "hybrid");
let mut m = cantilever();
m.replace_material("Steel", Material { density: None, ..steel() }).unwrap();
unsupported(&m, "density");
let mut m = cantilever_modes();
m.replace_coupling(
"pad",
Coupling { reference_point: "RP".into(), set: "tip".into(), kind: CouplingKind::Kinematic, dofs: Dofs::try_from(vec![1, 2, 3]).unwrap() },
)
.unwrap();
unsupported(&m, "coupling `pad`");
let mut m = cantilever_modes();
m.add_boundary_condition("hold_rp", BoundaryCondition::Fixed { target: Target::ReferencePoint("RP".into()) }).unwrap();
m.replace_step(step("Bend", Procedure::Static {}, &["clamp", "hold_rp"], &["push"])).unwrap();
unsupported(&m, "boundary condition `hold_rp`");
let mut m = plate();
m.replace_section(
"skin",
Section {
set: "plate".into(),
material: "Steel".into(),
kind: SectionKind::Shell { thickness: 0.5, offset: 0.0, points: 11 },
formulation: Formulation::default(),
},
)
.unwrap();
unsupported(&m, "11 integration points");
let mut m = plate();
m.add_set("half", Set { elements: vec![1], ..Set::default() }).unwrap();
m.replace_load("weight", Load::Gravity { acceleration: [0.0, 0.0, -9810.0], set: Some("half".into()) }).unwrap();
unsupported(&m, "gravity on set `half`");
}
#[test]
fn gravity_on_whole_sections_uses_a_part_set() {
let mut m = plate();
m.replace_load("weight", Load::Gravity { acceleration: [0.0, 0.0, -9810.0], set: Some("plate".into()) }).unwrap();
let w = lsdyna::write(&m).unwrap();
assert!(w.text.contains("*SET_PART_LIST") && w.text.contains("*LOAD_BODY_PARTS"));
}
#[test]
fn models_that_are_not_ready_are_refused_not_written() {
assert!(matches!(lsdyna::write(&Model::default()), Err(Error::Model(_))));
let mut m = cantilever();
m.remove_step("Bend").unwrap();
assert!(matches!(lsdyna::write(&m), Err(Error::Model(_))));
}
#[test]
fn hostile_numbers_stay_within_their_fields() {
let mut m = cantilever();
m.set_node(1, [1e9, -1e9, 1e-300]).unwrap();
m.set_node(2, [f64::MIN_POSITIVE, -f64::MIN_POSITIVE, 5e-324]).unwrap();
m.replace_material(
"Steel",
Material { elastic: Some(Elastic::Isotropic { young: 1e300, poisson: 0.499_999_999_999_999_9 }), density: Some(1e-300) },
)
.unwrap();
m.replace_load(
"tip_load",
Load::Force { target: Target::Set("tip".into()), vector: [1e-300, -1e300, 1.0 / 3.0], distribution: Distribution::PerNode },
)
.unwrap();
let w = lsdyna::write(&m).unwrap();
for line in w.text.lines().filter(|l| !l.starts_with('$') && !l.starts_with('*')) {
for f in fields(line) {
assert!(f.len() <= 20, "{line}");
}
}
let deck = read(&w.text);
assert_eq!(deck.nodes[&1], [1e9, -1e9, 1e-300]);
}
+93
View File
@@ -0,0 +1,93 @@
*KEYWORD long=y
$ Written by ModelCraft: implicit linear analysis, long format (20-column fields).
$ Units: mm-t-s-N-MPa (consistent; nothing is converted).
*NODE
$ nid x y z
1 0.0 0.0 0.0
2 1.0 0.0 0.0
3 2.0 0.0 0.0
4 3.0 0.0 0.0
5 4.0 0.0 0.0
6 0.0 1.0 0.0
7 1.0 1.0 0.0
8 2.0 1.0 0.0
9 3.0 1.0 0.0
10 4.0 1.0 0.0
11 0.0 0.0 1.0
12 1.0 0.0 1.0
13 2.0 0.0 1.0
14 3.0 0.0 1.0
15 4.0 0.0 1.0
16 0.0 1.0 1.0
17 1.0 1.0 1.0
18 2.0 1.0 1.0
19 3.0 1.0 1.0
20 4.0 1.0 1.0
*MAT_ELASTIC_TITLE
Steel
$ mid ro e pr
1 7.85e-9 210000.0 0.3
*SECTION_SOLID
$ secid elform
1 2
*PART
beam
$ pid secid mid eosid hgid
1 1 1
*ELEMENT_SOLID
$ eid pid
$ n1 n2 n3 n4 n5 n6 n7 n8 n9 n10
1 1
1 2 7 6 11 12 17 16
2 1
2 3 8 7 12 13 18 17
3 1
3 4 9 8 13 14 19 18
4 1
4 5 10 9 14 15 20 19
*SET_NODE_LIST_TITLE
root
$ sid
2
1 6 11 16
*SET_NODE_LIST_TITLE
tip
$ sid
3
5 10 15 20
*TITLE
Bend
*CONTROL_IMPLICIT_GENERAL
$ imflag dt0
1 1.0
*CONTROL_IMPLICIT_SOLUTION
$ nsolvr
1
*CONTROL_TERMINATION
$ endtim
1.0
*DEFINE_CURVE_TITLE
Bend ramp
$ lcid
1
$ a1 o1
0.0 0.0
1.0 1.0
*BOUNDARY_SPC_SET_ID
1clamp
$ nsid cid dofx dofy dofz dofrx dofry dofrz
2 1 1 1 1 1 1
$ load `tip_load`
*LOAD_NODE_SET
$ nsid dof lcid sf
3 3 1 -25.0
*DATABASE_BINARY_D3PLOT
$ dt
1.0
*DATABASE_SPCFORC
$ dt
1.0
*DATABASE_EXTENT_BINARY
$ neiph neips maxint strflg
1
*END
@@ -0,0 +1,124 @@
*KEYWORD long=y
$ Written by ModelCraft: implicit linear analysis, long format (20-column fields).
$ Units: mm-t-s-N-MPa (consistent; nothing is converted).
$ 2 steps are written as *CASE analyses: run LS-DYNA with CASE on its command line.
*NODE
$ nid x y z
1 0.0 0.0 0.0
2 1.0 0.0 0.0
3 2.0 0.0 0.0
4 3.0 0.0 0.0
5 4.0 0.0 0.0
6 0.0 1.0 0.0
7 1.0 1.0 0.0
8 2.0 1.0 0.0
9 3.0 1.0 0.0
10 4.0 1.0 0.0
11 0.0 0.0 1.0
12 1.0 0.0 1.0
13 2.0 0.0 1.0
14 3.0 0.0 1.0
15 4.0 0.0 1.0
16 0.0 1.0 1.0
17 1.0 1.0 1.0
18 2.0 1.0 1.0
19 3.0 1.0 1.0
20 4.0 1.0 1.0
99999999 5.0 0.5 0.5
*MAT_ORTHOTROPIC_ELASTIC_TITLE
Ply
$ mid ro ea eb ec prba prca prcb
1 1.6e-9 140000.0 10000.0 10000.0 0.02142857142857143 0.02142857142857143 0.45
$ gab gbc gca aopt
5000.0 3500.0 5000.0 2.0
$ xp yp zp a1 a2 a3
1.0 0.0 0.0
$ v1 v2 v3 d1 d2 d3
0.0 1.0 0.0
*SECTION_SOLID
$ secid elform
1 -18
*PART
beam
$ pid secid mid eosid hgid
1 1 1
*ELEMENT_SOLID
$ eid pid
$ n1 n2 n3 n4 n5 n6 n7 n8 n9 n10
1 1
1 2 7 6 11 12 17 16
2 1
2 3 8 7 12 13 18 17
3 1
3 4 9 8 13 14 19 18
4 1
4 5 10 9 14 15 20 19
*SET_NODE_LIST_TITLE
root
$ sid
2
1 6 11 16
*SET_NODE_LIST_TITLE
pad (rigid body)
$ sid
4
99999999 5 10 15 20
*CONSTRAINED_NODAL_RIGID_BODY_TITLE
pad
$ pid cid nsid
2 4
*CASE_BEGIN_1
*TITLE
Bend
*CONTROL_IMPLICIT_GENERAL
$ imflag dt0
1 1.0
*CONTROL_IMPLICIT_SOLUTION
$ nsolvr
1
*CONTROL_TERMINATION
$ endtim
1.0
*DEFINE_CURVE_TITLE
Bend ramp
$ lcid
1
$ a1 o1
0.0 0.0
1.0 1.0
*BOUNDARY_SPC_SET_ID
1clamp
$ nsid cid dofx dofy dofz dofrx dofry dofrz
2 1 1 1 1 1 1
*LOAD_NODE_POINT
$ nid dof lcid sf
99999999 2 1 -10.0
99999999 5 1 250.0
*DATABASE_BINARY_D3PLOT
$ dt
1.0
*DATABASE_SPCFORC
$ dt
1.0
*DATABASE_EXTENT_BINARY
$ neiph neips maxint strflg
1
*CASE_END_1
*CASE_BEGIN_2
*TITLE
Modes
*CONTROL_IMPLICIT_GENERAL
$ imflag dt0
1 1.0
*CONTROL_IMPLICIT_EIGENVALUE
$ neig center lflag lftend rflag rhtend
6 1 1.0 1 5000.0
*CONTROL_TERMINATION
$ endtim
1.0
*BOUNDARY_SPC_SET_ID
2clamp
$ nsid cid dofx dofy dofz dofrx dofry dofrz
2 1 1 1 1 1 1
*CASE_END_2
*END
+94
View File
@@ -0,0 +1,94 @@
*KEYWORD long=y
$ Written by ModelCraft: implicit linear analysis, long format (20-column fields).
$ Units: mm-t-s-N-MPa (consistent; nothing is converted).
*NODE
$ nid x y z
1 0.0 0.0 0.0
2 10.0 0.0 0.0
3 20.0 0.0 0.0
4 0.0 10.0 0.0
5 10.0 10.0 0.0
6 20.0 10.0 0.0
7 30.0 0.0 0.0
8 30.0 10.0 0.0
*MAT_ELASTIC_TITLE
Steel
$ mid ro e pr
1 7.85e-9 210000.0 0.3
*SECTION_SHELL
$ secid elform shrf nip
1 4 0.8333333333333334 5
$ t1 t2 t3 t4 nloc
0.5 0.5
*SECTION_SHELL
$ secid elform shrf nip
2 16 0.8333333333333334 5
$ t1 t2 t3 t4 nloc
0.5 0.5
*PART
skin tri3
$ pid secid mid eosid hgid
1 1 1
*PART
skin quad4
$ pid secid mid eosid hgid
2 2 1
*ELEMENT_SHELL
$ eid pid n1 n2 n3 n4 n5 n6 n7 n8
1 2 1 2 5 4
2 2 2 3 6 5
3 1 3 7 8 8
4 1 3 8 6 6
*SET_NODE_LIST_TITLE
edge
$ sid
1
1 4
*SET_SEGMENT_TITLE
top
$ sid
3
$ n1 n2 n3 n4
1 2 5 4
2 3 6 5
3 7 8 8
3 8 6 6
*TITLE
Sag
*CONTROL_IMPLICIT_GENERAL
$ imflag dt0
1 1.0
*CONTROL_IMPLICIT_SOLUTION
$ nsolvr
1
*CONTROL_TERMINATION
$ endtim
1.0
*DEFINE_CURVE_TITLE
Sag ramp
$ lcid
1
$ a1 o1
0.0 0.0
1.0 1.0
*BOUNDARY_SPC_SET_ID
1hold
$ nsid cid dofx dofy dofz dofrx dofry dofrz
1 1 1 1 1 1 1
$ load `press`
*LOAD_SEGMENT_SET
$ ssid lcid sf
3 1 0.01
*LOAD_BODY_Z
$ lcid sf
1 9810.0
*DATABASE_BINARY_D3PLOT
$ dt
1.0
*DATABASE_SPCFORC
$ dt
1.0
*DATABASE_EXTENT_BINARY
$ neiph neips maxint strflg
1
*END
+162
View File
@@ -0,0 +1,162 @@
*KEYWORD long=y
$ Written by ModelCraft: implicit linear analysis, long format (20-column fields).
$ Units: m-kg-s-N-Pa (consistent; nothing is converted).
*NODE
$ nid x y z
1 0.0 0.0 0.0
2 1.0 0.0 0.0
3 0.0 1.0 0.0
4 0.0 0.0 1.0
5 2.0 0.0 0.0
6 3.0 0.0 0.0
7 2.0 1.0 0.0
8 2.0 0.0 1.0
9 2.5 0.0 0.0
10 2.5 0.5 0.0
11 2.0 0.5 0.0
12 2.0 0.0 0.5
13 2.5 0.0 0.5
14 2.0 0.5 0.5
15 4.0 0.0 0.0
16 5.0 0.0 0.0
17 4.0 1.0 0.0
18 4.0 0.0 1.0
19 5.0 0.0 1.0
20 4.0 1.0 1.0
21 6.0 0.0 0.0
22 7.0 0.0 0.0
23 7.0 1.0 0.0
24 6.0 1.0 0.0
25 6.0 0.0 1.0
26 7.0 0.0 1.0
27 7.0 1.0 1.0
28 6.0 1.0 1.0
29 6.5 0.0 0.0
30 7.0 0.5 0.0
31 6.5 1.0 0.0
32 6.0 0.5 0.0
33 6.5 0.0 1.0
34 7.0 0.5 1.0
35 6.5 1.0 1.0
36 6.0 0.5 1.0
37 6.0 0.0 0.5
38 7.0 0.0 0.5
39 7.0 1.0 0.5
40 6.0 1.0 0.5
99999999 6.5 0.5 2.0
*MAT_ELASTIC_TITLE
Aluminium
$ mid ro e pr
1 2700.0 70000000000.0 0.33
*SECTION_SOLID
$ secid elform
1 23
*SECTION_SOLID
$ secid elform
2 16
*SECTION_SOLID
$ secid elform
3 13
*SECTION_SOLID
$ secid elform
4 2
*PART
hex20
$ pid secid mid eosid hgid
1 1 1
*PART
tet10
$ pid secid mid eosid hgid
2 2 1
*PART
tet4
$ pid secid mid eosid hgid
3 3 1
*PART
wedge
$ pid secid mid eosid hgid
4 4 1
*ELEMENT_SOLID
$ eid pid
$ n1 n2 n3 n4 n5 n6 n7 n8 n9 n10
1 3
1 2 3 4 4 4 4 4
2 2
5 6 7 8 9 10 11 13 14 12
3 4
16 15 18 19 17 17 20 20
*ELEMENT_SOLID_H20
$ eid pid
$ n1 n2 n3 n4 n5 n6 n7 n8 n9 n10
4 1
21 22 23 24 25 26 27 28 29 30
31 32 33 34 35 36 37 38 39 40
*SET_NODE_LIST_TITLE
base
$ sid
1
1 2 3 4
*SET_NODE_LIST_TITLE
lid
$ sid
3
25 26 27 28 33 34 35 36
*SET_NODE_LIST_TITLE
tet10
$ sid
4
5 6 7 8 9 10 11 12
13 14
$ coupling `spider`
*CONSTRAINED_INTERPOLATION
$ icid dnid ddof cidd ityp idnsw fgm
1 99999999 123456 1 1
$ inid idof
3 123
*TITLE
Load
*CONTROL_IMPLICIT_GENERAL
$ imflag dt0
1 1.0
*CONTROL_IMPLICIT_SOLUTION
$ nsolvr
1
*CONTROL_TERMINATION
$ endtim
1.0
*DEFINE_CURVE_TITLE
Load ramp
$ lcid
1
$ a1 o1
0.0 0.0
1.0 1.0
*BOUNDARY_SPC_SET_ID
1hold
$ nsid cid dofx dofy dofz dofrx dofry dofrz
1 1 1 1 1 1 1
*BOUNDARY_PRESCRIBED_MOTION_SET_ID
2lift
$ nsid dof vad lcid sf
4 3 2 1 0.001
*LOAD_NODE_POINT
$ nid dof lcid sf
25 1 1 -83.33333333333337
26 1 1 -83.33333333333336
27 1 1 -83.33333333333323
28 1 1 -83.3333333333333
33 1 1 333.3333333333333
34 1 1 333.33333333333337
35 1 1 333.3333333333334
36 1 1 333.3333333333333
99999999 3 1 50.0
*DATABASE_BINARY_D3PLOT
$ dt
1.0
*DATABASE_SPCFORC
$ dt
1.0
*DATABASE_EXTENT_BINARY
$ neiph neips maxint strflg
1
*END