forked from 1b-agent/fieldcraft-upstream
* 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>
167 lines
8.4 KiB
Rust
167 lines
8.4 KiB
Rust
//! Trusses (rod3), point masses, rotary inertia, springs and dashpots in every deck format:
|
||
//! what each writes, and what each reports as unsupported.
|
||
|
||
use modelcraft_decks::{Error, abaqus, lsdyna, nastran};
|
||
use modelcraft_model::{
|
||
BoundaryCondition, Direction, Elastic, ElementKind, Formulation, Mass, Material, Model, PointMass, Procedure, ReferencePoint, Section,
|
||
SectionKind, Set, Spring, Step, Target, UnitSystem,
|
||
};
|
||
|
||
/// A rod2 (or rod3) bar from node 1 to node 2 and a rod2 on to node 4, a reference point beyond
|
||
/// it; a motor mass at the tip, a hub with inertia on the point, a spring to the ground across
|
||
/// the tip, a spring along the line from the tip to the hub, a dashpot to the ground.
|
||
fn model(rod3: bool) -> Model {
|
||
let mut m = Model::default();
|
||
m.set_units(UnitSystem::MmTonneS);
|
||
for (l, x) in [(1, 0.0), (2, 1000.0), (3, 500.0), (4, 2000.0)] {
|
||
m.set_node(l, [x, 0.0, 0.0]).unwrap();
|
||
}
|
||
if rod3 {
|
||
m.set_element(1, ElementKind::Rod3, vec![1, 2, 3]).unwrap();
|
||
} else {
|
||
m.set_element(1, ElementKind::Rod2, vec![1, 2]).unwrap();
|
||
}
|
||
m.set_element(2, ElementKind::Rod2, vec![2, 4]).unwrap();
|
||
m.add_set("bars", Set { elements: vec![1, 2], ..Set::default() }).unwrap();
|
||
m.add_set("base", Set { nodes: vec![1], ..Set::default() }).unwrap();
|
||
m.add_set("tip", Set { nodes: vec![4], ..Set::default() }).unwrap();
|
||
m.add_material(
|
||
"Steel",
|
||
Material { elastic: Some(Elastic::Isotropic { young: 210_000.0, poisson: 0.3 }), density: Some(7.85e-9), ..Material::default() },
|
||
)
|
||
.unwrap();
|
||
m.add_section(
|
||
"bars",
|
||
Section { set: "bars".into(), material: "Steel".into(), kind: SectionKind::Rod { area: 20.0 }, formulation: Formulation::default() },
|
||
)
|
||
.unwrap();
|
||
m.add_reference_point("Hub", ReferencePoint { xyz: [2500.0, 0.0, 0.0], label: 0 }).unwrap();
|
||
let tip = Target::Set("tip".into());
|
||
let hub = Target::ReferencePoint("Hub".into());
|
||
m.add_mass("motor", PointMass { target: tip.clone(), mass: Some(Mass::Isotropic(0.002)), inertia: None }).unwrap();
|
||
m.add_mass("hub", PointMass { target: hub.clone(), mass: Some(Mass::Isotropic(0.01)), inertia: Some([1.0, 2.0, 3.0, 0.1, 0.0, 0.0]) }).unwrap();
|
||
m.add_spring("mount", Spring::Spring { first: tip.clone(), second: None, direction: Direction::Dof(2), stiffness: 50.0 }).unwrap();
|
||
m.add_spring("link", Spring::Spring { first: tip.clone(), second: Some(hub), direction: Direction::Line, stiffness: 1000.0 }).unwrap();
|
||
m.add_spring("damper", Spring::Dashpot { first: tip, second: None, direction: Direction::Vector([0.0, 0.0, -2.0]), coefficient: 0.5 }).unwrap();
|
||
m.add_boundary_condition("fix", BoundaryCondition::Fixed { target: Target::Set("base".into()) }).unwrap();
|
||
let mut step = Step::new("Modes", Procedure::Frequency { modes: 3, min_hz: None, max_hz: None });
|
||
step.boundary_conditions.insert("fix".into());
|
||
m.add_step(step, None).unwrap();
|
||
m
|
||
}
|
||
|
||
fn unsupported(e: std::result::Result<impl std::fmt::Debug, Error>) -> String {
|
||
match e {
|
||
Err(Error::Unsupported { what, .. }) => what,
|
||
other => panic!("expected unsupported, got {other:?}"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn abaqus_writes_every_discrete_element_and_rod3() {
|
||
let deck = abaqus::write(&model(true)).unwrap();
|
||
for expected in [
|
||
"*ELEMENT, TYPE=T3D3\n1, 1, 2, 3\n",
|
||
"*ELEMENT, TYPE=MASS, ELSET=hub-mass\n3, 99999999\n",
|
||
"*ELEMENT, TYPE=MASS, ELSET=motor-mass\n4, 4\n",
|
||
"*ELEMENT, TYPE=ROTARYI, ELSET=hub-inertia\n5, 99999999\n",
|
||
"*ELEMENT, TYPE=DASHPOT1, ELSET=damper-dashpot\n6, 4\n",
|
||
"*ELEMENT, TYPE=SPRINGA, ELSET=link-spring\n7, 4, 99999999\n",
|
||
"*ELEMENT, TYPE=SPRING1, ELSET=mount-spring\n8, 4\n",
|
||
"*MASS, ELSET=hub-mass\n0.01\n",
|
||
"*ROTARY INERTIA, ELSET=hub-inertia\n1.0, 2.0, 3.0, 0.1, 0.0, 0.0\n",
|
||
"*DASHPOT, ELSET=damper-dashpot\n3\n0.5\n",
|
||
"*SPRING, ELSET=link-spring\n,\n1000.0\n",
|
||
"*SPRING, ELSET=mount-spring\n2\n50.0\n",
|
||
] {
|
||
assert!(deck.contains(expected), "{expected}\n{deck}");
|
||
}
|
||
// An element set name that a model set already has is made unique.
|
||
let mut m = model(false);
|
||
m.add_set("motor-mass", Set { nodes: vec![4], ..Set::default() }).unwrap();
|
||
assert!(abaqus::write(&m).unwrap().contains("ELSET=motor-mass-2\n"));
|
||
// Skew directions and masses per direction need orientations.
|
||
let mut m = model(false);
|
||
m.replace_spring(
|
||
"mount",
|
||
Spring::Spring { first: Target::Set("tip".into()), second: None, direction: Direction::Vector([0.0, 1.0, 1.0]), stiffness: 1.0 },
|
||
)
|
||
.unwrap();
|
||
m.replace_mass("motor", PointMass { target: Target::Set("tip".into()), mass: Some(Mass::Directional([1.0, 2.0, 3.0])), inertia: None }).unwrap();
|
||
let what = unsupported(abaqus::write(&m));
|
||
assert!(what.contains("spring `mount`") && what.contains("point mass `motor`"), "{what}");
|
||
}
|
||
|
||
#[test]
|
||
fn nastran_writes_conm1_celas2_cdamp2() {
|
||
let deck = nastran::write(&model(false)).unwrap();
|
||
eprintln!("{deck}");
|
||
for expected in ["CONM1*", "CELAS2*", "CDAMP2*", "$ point mass hub", "$ spring link", "$ dashpot damper"] {
|
||
assert!(deck.contains(expected), "{expected}\n{deck}");
|
||
}
|
||
// The hub's 6 × 6 mass matrix: mass on the translations, the inertia tensor on the rotations.
|
||
let conm1: Vec<&str> = deck.lines().skip_while(|l| !l.starts_with("CONM1*")).take(6).collect();
|
||
let fields: Vec<f64> = conm1
|
||
.iter()
|
||
.flat_map(|l| l.get(8..).unwrap_or("").as_bytes().chunks(16).map(|c| std::str::from_utf8(c).unwrap().trim().to_string()))
|
||
.filter(|t| !t.is_empty())
|
||
.filter_map(|t| t.parse().ok())
|
||
.collect();
|
||
assert_eq!(fields.first().copied(), Some(3.0), "element id after the mesh's: {conm1:?}");
|
||
assert!(
|
||
fields.ends_with(&[0.01, 0.0, 0.01, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.1, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.0]),
|
||
"{fields:?}"
|
||
);
|
||
// The line spring runs along X: component 1 at both grids; the dashpot along −Z: component 3.
|
||
let link = deck.lines().skip_while(|l| !l.contains("spring link")).nth(1).unwrap().to_string();
|
||
assert!(link.starts_with("CELAS2*") && link.contains("1000.") && link.ends_with("4 1"), "{link}");
|
||
let rest = deck.lines().skip_while(|l| !l.contains("spring link")).nth(2).unwrap().to_string();
|
||
assert!(rest.contains("99999999") && rest.trim_end().ends_with('1'), "{rest}");
|
||
let what = unsupported(nastran::write(&model(true)));
|
||
assert!(what.contains("rod3"), "{what}");
|
||
let mut m = model(false);
|
||
m.replace_spring(
|
||
"link",
|
||
Spring::Spring { first: Target::Set("tip".into()), second: None, direction: Direction::Vector([1.0, 1.0, 0.0]), stiffness: 1.0 },
|
||
)
|
||
.unwrap();
|
||
assert!(unsupported(nastran::write(&m)).contains("spring `link`"));
|
||
}
|
||
|
||
#[test]
|
||
fn lsdyna_writes_masses_inertia_and_discrete_springs() {
|
||
let deck = lsdyna::write(&model(false)).unwrap().text;
|
||
eprintln!("{deck}");
|
||
for expected in [
|
||
"*ELEMENT_MASS",
|
||
"*ELEMENT_INERTIA",
|
||
"*MAT_SPRING_ELASTIC_TITLE\nlink\n",
|
||
"*MAT_DAMPER_VISCOUS_TITLE\ndamper\n",
|
||
"*SECTION_DISCRETE",
|
||
"*DEFINE_SD_ORIENTATION",
|
||
"*ELEMENT_DISCRETE",
|
||
"*BOUNDARY_SPC_NODE",
|
||
] {
|
||
assert!(deck.contains(expected), "{expected}\n{deck}");
|
||
}
|
||
// Two grounded links (damper, mount) get two ground nodes after the mesh's (5, 6); the
|
||
// line spring has no orientation vector (vid 0, along its nodes).
|
||
let discrete: Vec<Vec<i64>> = deck
|
||
.lines()
|
||
.skip_while(|l| *l != "*ELEMENT_DISCRETE")
|
||
.skip(1)
|
||
.take_while(|l| !l.starts_with('*'))
|
||
.filter(|l| !l.starts_with('$'))
|
||
.map(|l| l.split_whitespace().map(|f| f.parse().unwrap()).collect())
|
||
.collect();
|
||
assert_eq!(discrete.len(), 3);
|
||
assert_eq!(discrete[0][2..4], [4, 5], "damper to its ground node");
|
||
assert_eq!(discrete[1][2..4], [4, 99_999_999], "link to the hub");
|
||
assert_eq!(discrete[1].get(4).copied().unwrap_or(0), 0, "along the line");
|
||
assert_eq!(discrete[2][2..4], [4, 6], "mount to its ground node");
|
||
assert!(unsupported(lsdyna::write(&model(true))).contains("rod3"));
|
||
let mut m = model(false);
|
||
m.replace_mass("motor", PointMass { target: Target::Set("tip".into()), mass: Some(Mass::Directional([1.0, 2.0, 3.0])), inertia: None }).unwrap();
|
||
assert!(unsupported(lsdyna::write(&m)).contains("point mass `motor`"));
|
||
}
|