forked from 1b-agent/fieldcraft-upstream
1D elements I: trusses (rod2, rod3), point masses, rotary inertia, springs and dashpots (#38)
* Model, fem and decks: point masses, rotary inertia, springs, dashpots and rod3 trusses Model: PointMass and Spring definitions (spring | dashpot; to ground or two-point; along a degree of freedom, a vector or the line of the ends) on a set or reference point, resolved per node; ElementKind::Rod3. fem: rod2/rod3 truss kernel on Basis::Line2/Line3 declaring ElementDofs (translations). Decks: Abaqus T3D3, MASS, ROTARYI, SPRING1/2/A, DASHPOT1/2/A; Nastran CONM1, CELAS2, CDAMP2; LS-DYNA ELEMENT_MASS/INERTIA and ELEMENT_DISCRETE; what a format cannot take is reported. Commands model.mass.* and model.spring.*. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Implicit solver: trusses, point masses, rotary inertia and springs on the DofLayout Trusses declare their translations like solids; point masses, inertia and springs ask the layout for the kinds they need at their nodes (translations, rotations), and reference points they use get six degrees of freedom. The matrix pattern takes rows for those points; dashpots are ignored with a warning. Existing results are bit-identical (hash-checked). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Explicit solver: trusses, point masses, springs and dashpots on the node-major layout Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs: fem.md on trusses and discrete elements in the layout; regenerate features.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs: trusses in plane models and corotational explicit trusses 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:
53 files changed
+3626
-110
No files matched your search
+139
-10
@@ -63,6 +63,18 @@
|
||||
//! | tri3, quad4 | | S3 / S3R, S4 / S4R | | |
|
||||
//! | quad8 | | S8R (reduced only) | | |
|
||||
//! | beam2, rod2 | | | B31 | T3D2 |
|
||||
//! | rod3 | | | | T3D3 |
|
||||
//!
|
||||
//! Trusses in a 2D model are T2D2 and T2D3.
|
||||
//!
|
||||
//! Point masses, springs and dashpots are elements numbered after the mesh's, one per resolved
|
||||
//! copy, in an element set per definition (`<name>-mass`, `<name>-inertia`, `<name>-spring`, made
|
||||
//! unique against the model's sets): `MASS` with `*MASS` (one mass for every direction),
|
||||
//! `ROTARYI` with `*ROTARY INERTIA` (I11, I22, I33, I12, I13, I23), and `SPRING1`/`DASHPOT1` to
|
||||
//! the ground and `SPRING2`/`DASHPOT2` between two nodes along a global degree of freedom, or
|
||||
//! `SPRINGA`/`DASHPOTA` along the line of the nodes, with `*SPRING`/`*DASHPOT`. A mass that
|
||||
//! differs by direction and a spring along a skew fixed vector are reported (they need a local
|
||||
//! orientation, not written yet).
|
||||
//!
|
||||
//! | ModelCraft | Plane stress | Plane strain | Axisymmetric |
|
||||
//! |---|---|---|---|
|
||||
@@ -137,13 +149,16 @@ pub fn write_resolved(model: &Model, r: &Resolved) -> Result<String> {
|
||||
crate::refuse_fluid(FORMAT, model)?;
|
||||
let by_type = elements_by_type(model, r)?;
|
||||
check_expressible(model)?;
|
||||
let discrete = discrete_groups(model, r)?;
|
||||
let mut d = Deck::default();
|
||||
d.heading(model);
|
||||
d.nodes(model)?;
|
||||
d.elements(&by_type);
|
||||
d.discrete_elements(&discrete);
|
||||
d.sets(model, r)?;
|
||||
d.materials(model)?;
|
||||
d.sections(model)?;
|
||||
d.discrete_properties(&discrete)?;
|
||||
d.couplings(model, r);
|
||||
d.steps(model, r)?;
|
||||
Ok(d.out)
|
||||
@@ -198,6 +213,7 @@ pub fn element_type(kind: ElementKind, section: &Section) -> std::result::Result
|
||||
(S::Shell { .. }, K::Tri6, ..) => return Err("tri6 shells (Abaqus has no general-purpose six-node triangular shell)".into()),
|
||||
(S::Beam { .. }, K::Beam2, ..) => "B31",
|
||||
(S::Rod { .. }, K::Rod2, ..) => "T3D2",
|
||||
(S::Rod { .. }, K::Rod3, ..) => "T3D3",
|
||||
(s, ..) => return Err(format!("{} elements in a {} section", kind.name(), s.name())),
|
||||
};
|
||||
Ok(if f.hybrid { format!("{plain}H") } else { plain.to_string() })
|
||||
@@ -216,7 +232,9 @@ fn elements_by_type<'m>(model: &'m Model, r: &Resolved) -> Result<BTreeMap<Strin
|
||||
}
|
||||
}
|
||||
for (kind, elements) in by_kind {
|
||||
match element_type(kind, section) {
|
||||
// Trusses of a 2D model are the planar ones.
|
||||
let planar = |t: String| if r.is_planar() { t.replacen("T3D", "T2D", 1) } else { t };
|
||||
match element_type(kind, section).map(planar) {
|
||||
Ok(t) => {
|
||||
count += elements.len();
|
||||
by_type.entry(t).or_default().extend(elements);
|
||||
@@ -284,6 +302,90 @@ fn check_expressible(model: &Model) -> Result<()> {
|
||||
unsupported(FORMAT, bad)
|
||||
}
|
||||
|
||||
/// The property of a group of discrete elements.
|
||||
enum DiscreteProperty {
|
||||
Mass(f64),
|
||||
Inertia([f64; 6]),
|
||||
/// `*SPRING` or `*DASHPOT`: the degrees of freedom line (empty for the axial form) and the
|
||||
/// stiffness or damping coefficient.
|
||||
Spring {
|
||||
dashpot: bool,
|
||||
dofs: String,
|
||||
value: f64,
|
||||
},
|
||||
}
|
||||
|
||||
/// The discrete elements of one definition: element type, element set, members and property.
|
||||
struct DiscreteGroup {
|
||||
element_type: &'static str,
|
||||
elset: String,
|
||||
members: Vec<(u32, Vec<u32>)>,
|
||||
property: DiscreteProperty,
|
||||
}
|
||||
|
||||
/// Point masses, springs and dashpots as element groups (one per definition and element type,
|
||||
/// labels counting up after the mesh's), or everything Abaqus cannot take.
|
||||
fn discrete_groups<'r>(model: &Model, r: &'r Resolved) -> Result<Vec<DiscreteGroup>> {
|
||||
let mut next = model.elements().keys().next_back().map_or(1, |l| l.saturating_add(1));
|
||||
let mut label = || -> Result<u32> {
|
||||
let l = next;
|
||||
if l > modelcraft_model::MAX_LABEL {
|
||||
return Err(Error::Limit(format!(
|
||||
"no element label left below {} for point masses, springs and dashpots",
|
||||
modelcraft_model::MAX_LABEL as u64 + 1
|
||||
)));
|
||||
}
|
||||
next += 1;
|
||||
Ok(l)
|
||||
};
|
||||
let mut taken: BTreeSet<String> = model.sets().keys().map(|n| n.to_ascii_uppercase()).collect();
|
||||
let mut groups: Vec<DiscreteGroup> = Vec::new();
|
||||
// The (definition, element type) the last group holds.
|
||||
let mut current: Option<(&str, &'static str)> = None;
|
||||
let mut push = |name: &'r str, suffix: &str, element_type: &'static str, property: DiscreteProperty, member: (u32, Vec<u32>)| {
|
||||
let key = (name, element_type);
|
||||
if current.as_ref() == Some(&key)
|
||||
&& let Some(g) = groups.last_mut()
|
||||
{
|
||||
g.members.push(member);
|
||||
return;
|
||||
}
|
||||
let elset = crate::unique_name(&format!("{name}-{suffix}"), &mut taken);
|
||||
groups.push(DiscreteGroup { element_type, elset, members: vec![member], property });
|
||||
current = Some(key);
|
||||
};
|
||||
let mut bad = BTreeSet::new();
|
||||
for m in &r.masses {
|
||||
if m.mass.iter().any(|c| *c != 0.0) {
|
||||
if m.mass[0] != m.mass[1] || m.mass[0] != m.mass[2] {
|
||||
bad.insert(format!("point mass `{}`: a mass that differs by direction (needs a local orientation, not written yet)", m.name));
|
||||
}
|
||||
push(&m.name, "mass", "MASS", DiscreteProperty::Mass(m.mass[0]), (label()?, vec![m.node]));
|
||||
}
|
||||
}
|
||||
for m in &r.masses {
|
||||
if m.inertia.iter().any(|c| *c != 0.0) {
|
||||
push(&m.name, "inertia", "ROTARYI", DiscreteProperty::Inertia(m.inertia), (label()?, vec![m.node]));
|
||||
}
|
||||
}
|
||||
for s in &r.springs {
|
||||
let what = s.kind();
|
||||
let line = matches!(s.direction, modelcraft_model::ResolvedDirection::Line(_));
|
||||
let (element_type, dofs, nodes) = match (s.second, crate::axis_dof(s.direction), line) {
|
||||
(Some(b), _, true) => (if s.dashpot { "DASHPOTA" } else { "SPRINGA" }, String::new(), vec![s.first, b]),
|
||||
(Some(b), Some(d), false) => (if s.dashpot { "DASHPOT2" } else { "SPRING2" }, format!("{d}, {d}"), vec![s.first, b]),
|
||||
(None, Some(d), _) => (if s.dashpot { "DASHPOT1" } else { "SPRING1" }, d.to_string(), vec![s.first]),
|
||||
(_, None, _) => {
|
||||
bad.insert(format!("{what} `{}`: a direction along no global axis (needs a local orientation, not written yet)", s.name));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
push(&s.name, what, element_type, DiscreteProperty::Spring { dashpot: s.dashpot, dofs, value: s.value }, (label()?, nodes));
|
||||
}
|
||||
unsupported(FORMAT, bad)?;
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
/// Append a real number in the shortest form that reads back to the same value: `0.1`,
|
||||
/// `210000.0`, `7.85E-9`, `1.0E300`.
|
||||
fn push_number(out: &mut String, v: f64) -> Result<()> {
|
||||
@@ -342,15 +444,7 @@ fn coupling_surfaces(model: &Model, r: &Resolved) -> BTreeMap<String, 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");
|
||||
let mut candidate = base.clone();
|
||||
let mut k = 2u32;
|
||||
while taken.contains(&candidate.to_ascii_uppercase()) {
|
||||
candidate = format!("{base}-{k}");
|
||||
k = k.saturating_add(1);
|
||||
}
|
||||
taken.insert(candidate.to_ascii_uppercase());
|
||||
out.insert(name.clone(), candidate);
|
||||
out.insert(name.clone(), crate::unique_name(&format!("{name}-nodes"), &mut taken));
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -581,6 +675,41 @@ impl Deck {
|
||||
}
|
||||
}
|
||||
|
||||
fn discrete_elements(&mut self, groups: &[DiscreteGroup]) {
|
||||
if groups.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.comment("Point masses, springs and dashpots");
|
||||
for g in groups {
|
||||
self.line(&format!("*ELEMENT, TYPE={}, ELSET={}", g.element_type, g.elset));
|
||||
for (label, nodes) in &g.members {
|
||||
let nodes: Vec<String> = nodes.iter().map(u32::to_string).collect();
|
||||
self.line(&format!("{label}, {}", nodes.join(", ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn discrete_properties(&mut self, groups: &[DiscreteGroup]) -> Result<()> {
|
||||
for g in groups {
|
||||
match &g.property {
|
||||
DiscreteProperty::Mass(m) => {
|
||||
self.line(&format!("*MASS, ELSET={}", g.elset));
|
||||
self.numbers_line("", &[*m])?;
|
||||
}
|
||||
DiscreteProperty::Inertia(i) => {
|
||||
self.line(&format!("*ROTARY INERTIA, ELSET={}", g.elset));
|
||||
self.numbers_line("", i)?;
|
||||
}
|
||||
DiscreteProperty::Spring { dashpot, dofs, value } => {
|
||||
self.line(&format!("{}, ELSET={}", if *dashpot { "*DASHPOT" } else { "*SPRING" }, g.elset));
|
||||
self.line(if dofs.is_empty() { "," } else { dofs });
|
||||
self.numbers_line("", &[*value])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sets(&mut self, model: &Model, r: &Resolved) -> Result<()> {
|
||||
if r.sets.is_empty() {
|
||||
return Ok(());
|
||||
|
||||
+26
-1
@@ -16,7 +16,7 @@ pub mod nastran;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use modelcraft_model::nodal::edge_pressure;
|
||||
use modelcraft_model::{Diagnostic, Load, Model, Resolved};
|
||||
use modelcraft_model::{Diagnostic, Load, Model, Resolved, ResolvedDirection};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
@@ -158,3 +158,28 @@ pub fn edge_pressure_loads<'a>(model: &Model, r: &Resolved, active: impl IntoIte
|
||||
out.retain(|_, v| *v != 0.0);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The global degree of freedom (1–6) a spring or dashpot direction acts along, when it is one
|
||||
/// (a vector or line along an axis counts: the sign does not matter to a spring).
|
||||
pub(crate) fn axis_dof(d: ResolvedDirection) -> Option<u8> {
|
||||
match d {
|
||||
ResolvedDirection::Dof(k) => Some(k),
|
||||
ResolvedDirection::Vector(n) | ResolvedDirection::Line(n) => {
|
||||
let axis = n.iter().position(|c| c.abs() == 1.0)?;
|
||||
(n.iter().filter(|c| **c != 0.0).count() == 1).then_some(axis as u8 + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `base`, or `base-2`, `base-3`, … : the first that is not in `taken` (compared without case),
|
||||
/// which it is then added to.
|
||||
pub(crate) fn unique_name(base: &str, taken: &mut BTreeSet<String>) -> String {
|
||||
let mut candidate = base.to_string();
|
||||
let mut k = 2u32;
|
||||
while taken.contains(&candidate.to_ascii_uppercase()) {
|
||||
candidate = format!("{base}-{k}");
|
||||
k = k.saturating_add(1);
|
||||
}
|
||||
taken.insert(candidate.to_ascii_uppercase());
|
||||
candidate
|
||||
}
|
||||
@@ -38,7 +38,7 @@ impl Keyword {
|
||||
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,
|
||||
ElementKind::Rod2 | ElementKind::Rod3 | ElementKind::Beam2 => Keyword::Beam,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ impl Keyword {
|
||||
pub(crate) fn order(kind: ElementKind) -> &'static [usize] {
|
||||
match kind {
|
||||
ElementKind::Rod2 | ElementKind::Beam2 => &[0, 1],
|
||||
ElementKind::Rod3 => &[0, 1, 2],
|
||||
ElementKind::Tri3 => &[0, 1, 2, 2],
|
||||
ElementKind::Tri6 => &[0, 1, 2, 2, 3, 4, 5, 5],
|
||||
ElementKind::Quad4 => &[0, 1, 2, 3],
|
||||
@@ -141,6 +142,7 @@ pub(crate) fn elform(kind: ElementKind, section: &SectionKind, f: Formulation, s
|
||||
},
|
||||
SectionKind::Rod { .. } => match kind {
|
||||
ElementKind::Rod2 => plain(3),
|
||||
ElementKind::Rod3 => no("LS-DYNA's trusses have two nodes; there is no counterpart for"),
|
||||
_ => no("a rod section cannot hold"),
|
||||
},
|
||||
SectionKind::Beam { .. } => no("beam sections are not written to LS-DYNA decks yet (planned with beam profiles):"),
|
||||
|
||||
@@ -31,6 +31,15 @@
|
||||
//! (with `*LOAD_BODY_PARTS` when it acts on a set); output as `*DATABASE_BINARY_D3PLOT`,
|
||||
//! `*DATABASE_SPCFORC` and `*DATABASE_EXTENT_BINARY`.
|
||||
//!
|
||||
//! Point masses are `*ELEMENT_MASS` (one mass for every direction) or `*ELEMENT_INERTIA` (the
|
||||
//! inertia tensor's components, with the mass); springs and dashpots are `*ELEMENT_DISCRETE` with
|
||||
//! one `*PART`, `*SECTION_DISCRETE` (torsional for a rotational degree of freedom) and
|
||||
//! `*MAT_SPRING_ELASTIC` or `*MAT_DAMPER_VISCOUS` per definition, acting along the line of their
|
||||
//! nodes or along a `*DEFINE_SD_ORIENTATION` vector. A spring or dashpot to the ground connects
|
||||
//! to an extra node at the same place, held by `*BOUNDARY_SPC_NODE` (labels after the mesh's).
|
||||
//! Element, part, section and material ids continue after the mesh's. A mass that differs by
|
||||
//! direction and the three-node truss (rod3) are reported.
|
||||
//!
|
||||
//! Several steps are independent analyses in the model (nothing carries over), which is what
|
||||
//! LS-DYNA's `*CASE` runs: each step is a case between `*CASE_BEGIN_n` and `*CASE_END_n`, run in
|
||||
//! order when LS-DYNA is started with `CASE` on its command line. A single step is written
|
||||
@@ -54,7 +63,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use modelcraft_model::{
|
||||
BoundaryCondition, CouplingKind, Diagnostic, Dofs, Elastic, Element, ElementKind, Hardening, Load, Material, Model, OutputVariable, Plastic,
|
||||
Procedure, Resolved, Section, SectionKind, Step, Target,
|
||||
Procedure, Resolved, ResolvedDirection, Section, SectionKind, Step, Target,
|
||||
};
|
||||
|
||||
use crate::{Error, Result};
|
||||
@@ -161,6 +170,7 @@ fn write_with(model: &Model, r: &Resolved) -> Result<Written> {
|
||||
element_cards(&mut d, &plan)?;
|
||||
sets(&mut d, model, r, &plan)?;
|
||||
couplings(&mut d, model, r, &plan)?;
|
||||
discrete(&mut d, model, r, &plan)?;
|
||||
if let Some(parts) = &plan.gravity_parts {
|
||||
d.line("*SET_PART_LIST");
|
||||
d.header(&["sid"]);
|
||||
@@ -312,6 +322,16 @@ fn plan<'a>(model: &'a Model, r: &Resolved) -> Result<Plan<'a>> {
|
||||
None
|
||||
}
|
||||
};
|
||||
if r.is_planar() {
|
||||
for name in model.sections().iter().filter(|(_, s)| matches!(s.kind, SectionKind::Rod { .. })).map(|(n, _)| n) {
|
||||
refused.insert(format!("section `{name}`: trusses in a 2D model (LS-DYNA's 2D elements take no truss)"));
|
||||
}
|
||||
}
|
||||
for m in &r.masses {
|
||||
if m.mass[0] != m.mass[1] || m.mass[0] != m.mass[2] {
|
||||
refused.insert(format!("point mass `{}`: a mass that differs by direction (LS-DYNA's mass elements take one mass)", m.name));
|
||||
}
|
||||
}
|
||||
let mut curves = BTreeMap::new();
|
||||
for name in materials.keys() {
|
||||
let Some(m) = model.material(name) else { continue };
|
||||
@@ -646,6 +666,122 @@ fn couplings(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan) -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Point masses, springs and dashpots (see the module docs).
|
||||
fn discrete(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan) -> Result<()> {
|
||||
if r.masses.is_empty() && r.springs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let limit = |what: &str| Error::Limit(format!("no {what} id left for point masses, springs and dashpots"));
|
||||
let mut eid = model.elements().keys().next_back().copied().unwrap_or(0);
|
||||
let mut next_eid = || -> Result<u32> {
|
||||
eid = eid.checked_add(1).filter(|e| *e <= modelcraft_model::MAX_LABEL).ok_or_else(|| limit("element"))?;
|
||||
Ok(eid)
|
||||
};
|
||||
d.comment("Point masses");
|
||||
for m in &r.masses {
|
||||
let mass = m.mass[0];
|
||||
if m.inertia.iter().all(|c| *c == 0.0) {
|
||||
d.line("*ELEMENT_MASS");
|
||||
d.header(&["eid", "nid", "mass"]);
|
||||
d.card([Field::from(next_eid()?), Field::from(m.node), Field::from(mass)]);
|
||||
} else {
|
||||
let [i11, i22, i33, i12, i13, i23] = m.inertia;
|
||||
d.line("*ELEMENT_INERTIA");
|
||||
d.header(&["eid", "nid", "csid"]);
|
||||
d.card([Field::from(next_eid()?), Field::from(m.node)]);
|
||||
d.header(&["ixx", "ixy", "ixz", "iyy", "iyz", "izz", "mass"]);
|
||||
d.card([i11, i12, i13, i22, i23, i33, mass].map(Field::from));
|
||||
}
|
||||
}
|
||||
if r.springs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
// Ids after the mesh's parts, the rigid bodies' parts and the materials.
|
||||
let kinematic = model.couplings().values().filter(|c| c.kind == CouplingKind::Kinematic).count();
|
||||
let mut pid = u32::try_from(plan.parts.len() + kinematic).map_err(|_| limit("part"))?;
|
||||
let mut mid = u32::try_from(plan.materials.len()).map_err(|_| limit("material"))?;
|
||||
let mut vid = 0u32;
|
||||
// Ground nodes: labels after the mesh's, skipping the reference points'.
|
||||
let taken: BTreeSet<u32> = model.reference_points().values().map(|p| p.label).collect();
|
||||
let mut node = model.nodes().keys().next_back().copied().unwrap_or(0);
|
||||
let mut ground: Vec<(u32, [f64; 3])> = Vec::new();
|
||||
let mut elements: Vec<Vec<Field>> = Vec::new();
|
||||
let mut current: Option<&str> = None;
|
||||
let mut vid_of_part = 0u32;
|
||||
for s in &r.springs {
|
||||
let rotational = s.direction.is_rotational();
|
||||
if current != Some(s.name.as_str()) {
|
||||
current = Some(&s.name);
|
||||
pid = pid.checked_add(1).ok_or_else(|| limit("part"))?;
|
||||
mid = mid.checked_add(1).ok_or_else(|| limit("material"))?;
|
||||
d.comment(&format!("{} `{}`", s.kind(), s.name));
|
||||
if s.dashpot {
|
||||
d.line("*MAT_DAMPER_VISCOUS_TITLE");
|
||||
d.line(&s.name);
|
||||
d.header(&["mid", "dc"]);
|
||||
} else {
|
||||
d.line("*MAT_SPRING_ELASTIC_TITLE");
|
||||
d.line(&s.name);
|
||||
d.header(&["mid", "k"]);
|
||||
}
|
||||
d.card([Field::from(mid), Field::from(s.value)]);
|
||||
d.line("*SECTION_DISCRETE");
|
||||
d.header(&["secid", "dro"]);
|
||||
d.card([Field::from(pid), Field::from(i32::from(rotational))]);
|
||||
d.line("*PART");
|
||||
d.line(&s.name);
|
||||
d.header(&["pid", "secid", "mid"]);
|
||||
d.card([Field::from(pid), Field::from(pid), Field::from(mid)]);
|
||||
let axis = (!matches!(s.direction, ResolvedDirection::Line(_))).then(|| s.direction.axis());
|
||||
let v = match axis {
|
||||
Some([x, y, z]) => {
|
||||
vid += 1;
|
||||
d.line("*DEFINE_SD_ORIENTATION");
|
||||
d.header(&["vid", "iop", "xt", "yt", "zt"]);
|
||||
d.card([Field::from(vid), Field::from(0), Field::from(x), Field::from(y), Field::from(z)]);
|
||||
vid
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
vid_of_part = v;
|
||||
}
|
||||
let second = match s.second {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
node = node.checked_add(1).ok_or_else(|| limit("node"))?;
|
||||
while taken.contains(&node) {
|
||||
node = node.checked_add(1).ok_or_else(|| limit("node"))?;
|
||||
}
|
||||
if node > modelcraft_model::MAX_LABEL {
|
||||
return Err(limit("node"));
|
||||
}
|
||||
ground.push((node, model.label_xyz(s.first).ok_or_else(|| broken(format!("node {}", s.first)))?));
|
||||
node
|
||||
}
|
||||
};
|
||||
elements.push(vec![Field::from(next_eid()?), Field::from(pid), Field::from(s.first), Field::from(second), Field::from(vid_of_part)]);
|
||||
}
|
||||
d.line("*ELEMENT_DISCRETE");
|
||||
d.header(&["eid", "pid", "n1", "n2", "vid"]);
|
||||
for e in elements {
|
||||
d.card(e);
|
||||
}
|
||||
if !ground.is_empty() {
|
||||
d.comment("The ground ends of springs and dashpots, held in every direction");
|
||||
d.line("*NODE");
|
||||
d.header(&["nid", "x", "y", "z"]);
|
||||
for (l, [x, y, z]) in &ground {
|
||||
d.card([Field::from(*l), Field::from(*x), Field::from(*y), Field::from(*z)]);
|
||||
}
|
||||
d.line("*BOUNDARY_SPC_NODE");
|
||||
d.header(&["nid", "cid", "dofx", "dofy", "dofz", "dofrx", "dofry", "dofrz"]);
|
||||
for (l, _) in &ground {
|
||||
d.card([Field::from(*l), Field::Blank].into_iter().chain([1; 6].map(Field::from)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// LS-DYNA's degree-of-freedom code for prescribed motion and nodal loads: 1–3 translations,
|
||||
/// 5–7 rotations.
|
||||
fn dof_code(dof: u8) -> i32 {
|
||||
|
||||
@@ -35,7 +35,13 @@
|
||||
//! (`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).
|
||||
//! revolutions per time unit);
|
||||
//! - point masses: `CONM1`, the node's 6 × 6 mass matrix (the masses per direction on the
|
||||
//! translations, the inertia tensor's components on the rotations, so no product-of-inertia sign
|
||||
//! convention is involved); springs and dashpots along a global degree of freedom: `CELAS2` and
|
||||
//! `CDAMP2` (the second grid left blank for one to the ground). Their ids follow the rigid
|
||||
//! elements'. A spring or dashpot along a skew direction and the three-node truss (rod3) have
|
||||
//! no counterpart here and are reported.
|
||||
//!
|
||||
//! 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.
|
||||
@@ -96,7 +102,7 @@ pub fn order(kind: ElementKind) -> &'static [usize] {
|
||||
/// The connection entry's name for an element kind.
|
||||
pub fn element_card(kind: ElementKind) -> &'static str {
|
||||
match kind {
|
||||
ElementKind::Rod2 => "CROD",
|
||||
ElementKind::Rod2 | ElementKind::Rod3 => "CROD",
|
||||
ElementKind::Beam2 => "CBEAM",
|
||||
ElementKind::Tri3 => "CTRIA3",
|
||||
ElementKind::Tri6 => "CTRIA6",
|
||||
@@ -257,6 +263,8 @@ struct Deck<'m> {
|
||||
element_pid: BTreeMap<u32, u32>,
|
||||
/// Rigid elements: (id, coupling name, coupling).
|
||||
rigid: Vec<(u32, &'m str, &'m Coupling)>,
|
||||
/// Point masses, springs and dashpots: (comment, card).
|
||||
discrete: Vec<(String, Card)>,
|
||||
/// Node → (dependent degrees of freedom, coupling that makes them dependent).
|
||||
dependent: BTreeMap<u32, (Dofs, &'m str)>,
|
||||
/// Constraint sets by id.
|
||||
@@ -304,6 +312,7 @@ impl<'m> Deck<'m> {
|
||||
properties: Vec::new(),
|
||||
element_pid: BTreeMap::new(),
|
||||
rigid: Vec::new(),
|
||||
discrete: Vec::new(),
|
||||
dependent: BTreeMap::new(),
|
||||
spc_sets: Vec::new(),
|
||||
load_sids: BTreeMap::new(),
|
||||
@@ -315,6 +324,7 @@ impl<'m> Deck<'m> {
|
||||
self.plan_materials()?;
|
||||
self.plan_sections()?;
|
||||
self.plan_couplings()?;
|
||||
self.plan_discrete()?;
|
||||
self.plan_constraints()?;
|
||||
self.plan_loads()?;
|
||||
for sub in &mut self.subcases {
|
||||
@@ -447,6 +457,9 @@ impl<'m> Deck<'m> {
|
||||
self.add_property(sname, Property::Beam { mid, profile, orientation }, elements.iter().copied())?;
|
||||
}
|
||||
SectionKind::Rod { area } => {
|
||||
if elements.iter().any(|e| kind_of(*e) == Some(ElementKind::Rod3)) {
|
||||
self.problems.insert(format!("section `{sname}`: three-node trusses (rod3; Nastran's rods have two grids)"));
|
||||
}
|
||||
let Some(mid) = ids.isotropic else { continue };
|
||||
self.add_property(sname, Property::Rod { mid, area }, elements.iter().copied())?;
|
||||
}
|
||||
@@ -520,6 +533,44 @@ impl<'m> Deck<'m> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Point masses, springs and dashpots, numbered after the rigid elements.
|
||||
fn plan_discrete(&mut self) -> Result<()> {
|
||||
let r = self.r;
|
||||
let mut next = self.rigid.last().map(|e| e.0).or_else(|| self.model.elements().keys().next_back().copied()).unwrap_or(0);
|
||||
let mut id = || -> Result<u32> {
|
||||
next = next
|
||||
.checked_add(1)
|
||||
.filter(|n| *n <= MAX_LABEL)
|
||||
.ok_or_else(|| Error::Limit(format!("no element id left below {} for point masses, springs and dashpots", MAX_LABEL as u64 + 1)))?;
|
||||
Ok(next)
|
||||
};
|
||||
for m in &r.masses {
|
||||
let [mx, my, mz] = m.mass;
|
||||
let [i11, i22, i33, i12, i13, i23] = m.inertia;
|
||||
// M11, M21, M22, M31, M32, M33, then the rotations' rows: M41…M44, M51…M55, M61…M66.
|
||||
let card = Card::new("CONM1")
|
||||
.int(id()?)
|
||||
.int(m.node)
|
||||
.blank()
|
||||
.reals([mx, 0.0, my, 0.0, 0.0, mz, 0.0, 0.0, 0.0, i11, 0.0, 0.0, 0.0, i12, i22, 0.0, 0.0, 0.0, i13, i23, i33]);
|
||||
self.discrete.push((format!("point mass {}", m.name), card));
|
||||
}
|
||||
for s in &r.springs {
|
||||
let what = s.kind();
|
||||
let Some(dof) = crate::axis_dof(s.direction) else {
|
||||
self.problems.insert(format!("{what} `{}`: a direction along no global axis (CELAS and CDAMP act along grid components)", s.name));
|
||||
continue;
|
||||
};
|
||||
let card = Card::new(if s.dashpot { "CDAMP2" } else { "CELAS2" }).int(id()?).real(s.value).int(s.first).int(dof);
|
||||
let card = match s.second {
|
||||
Some(b) => card.int(b).int(dof),
|
||||
None => card,
|
||||
};
|
||||
self.discrete.push((format!("{what} {}", s.name), card));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One constraint set per distinct step constraint, checked against coupled nodes.
|
||||
fn plan_constraints(&mut self) -> Result<()> {
|
||||
let (model, r) = (self.model, self.r);
|
||||
@@ -780,6 +831,13 @@ impl<'m> Deck<'m> {
|
||||
};
|
||||
write_named(out, &format!("coupling {name}"), *eid, card)?;
|
||||
}
|
||||
if !self.discrete.is_empty() {
|
||||
comment(out, "point masses, springs and dashpots");
|
||||
}
|
||||
for (what, card) in &self.discrete {
|
||||
comment(out, what);
|
||||
card.write(out)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user