decks: ties as Abaqus *TIE and LS-DYNA tied contact

Nastran refuses them by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 15:28:02 -04:00
1 parent 7e9366f726
commit 794aa9f158
4 files changed
+48 -2

No files matched your search

+20
View File
@@ -28,6 +28,11 @@
//! initial overclosure have no Abaqus counterpart and are noted in comments (Abaqus chooses its //! initial overclosure have no Abaqus counterpart and are noted in comments (Abaqus chooses its
//! own penalty and resolves overclosure in the first increment). //! own penalty and resolves overclosure in the first increment).
//! //!
//! # Ties
//!
//! Each tie is `*TIE, NAME=…, ADJUST=NO` with the secondary surface first and its position
//! tolerance when it has one: FieldCraft ties the nodes where they are (no adjustment).
//!
//! # Steps and the activity table //! # Steps and the activity table
//! //!
//! The model lists, for every step, exactly the boundary conditions and loads active in it; Abaqus //! The model lists, for every step, exactly the boundary conditions and loads active in it; Abaqus
@@ -187,6 +192,7 @@ pub fn write_resolved(model: &Model, r: &Resolved) -> Result<String> {
d.discrete_properties(&discrete)?; d.discrete_properties(&discrete)?;
d.couplings(model, r); d.couplings(model, r);
d.contacts(model, r)?; d.contacts(model, r)?;
d.ties(model)?;
d.steps(model, r)?; d.steps(model, r)?;
Ok(d.out) Ok(d.out)
} }
@@ -1042,6 +1048,20 @@ impl Deck {
Ok(()) Ok(())
} }
/// Ties: `*TIE` per tie, secondary surface first.
fn ties(&mut self, model: &Model) -> Result<()> {
if model.ties().is_empty() {
return Ok(());
}
self.comment("Ties: each secondary surface bonded to its main surface");
for (name, t) in model.ties() {
let tolerance = t.position_tolerance.map_or(String::new(), |v| format!(", POSITION TOLERANCE={v:?}"));
self.line(&format!("*TIE, NAME={name}, ADJUST=NO{tolerance}"));
self.line(&format!("{}, {}", t.secondary, t.main));
}
Ok(())
}
fn boundary_lines<'n>(&mut self, model: &Model, r: &Resolved, names: impl IntoIterator<Item = &'n String>) -> Result<()> { fn boundary_lines<'n>(&mut self, model: &Model, r: &Resolved, names: impl IntoIterator<Item = &'n String>) -> Result<()> {
for name in names { for name in names {
let Some(bc) = model.boundary_condition(name) else { continue }; let Some(bc) = model.boundary_condition(name) else { continue };
+20 -1
View File
@@ -23,7 +23,8 @@
//! node set), distributing as `*CONSTRAINED_INTERPOLATION` (the reference point dependent on //! node set), distributing as `*CONSTRAINED_INTERPOLATION` (the reference point dependent on
//! the set's translations, equal weights); //! the set's translations, equal weights);
//! - contact: a pair as `*CONTACT_AUTOMATIC_SURFACE_TO_SURFACE` between segment sets (the //! - contact: a pair as `*CONTACT_AUTOMATIC_SURFACE_TO_SURFACE` between segment sets (the
//! secondary surface first), general contact as `*CONTACT_AUTOMATIC_SINGLE_SURFACE` on a segment //! secondary surface first; a tie as `*CONTACT_TIED_SURFACE_TO_SURFACE` likewise), general
//! contact as `*CONTACT_AUTOMATIC_SINGLE_SURFACE` on a segment
//! set or on every part (`surfatyp` 5); the static and kinetic coefficients and the decay //! set or on every part (`surfatyp` 5); the static and kinetic coefficients and the decay
//! coefficient (`fs`, `fd`, `dc`: the same exponential decay law) and the penalty scale on both //! coefficient (`fs`, `fd`, `dc`: the same exponential decay law) and the penalty scale on both
//! sides (`sfsa`, `sfsb`, a factor on LS-DYNA's own default stiffness). LS-DYNA moves nodes //! sides (`sfsa`, `sfsb`, a factor on LS-DYNA's own default stiffness). LS-DYNA moves nodes
@@ -280,6 +281,9 @@ fn plan<'a>(model: &'a Model, r: &Resolved) -> Result<Plan<'a>> {
for c in model.contacts().values() { for c in model.contacts().values() {
segment_sets.extend(c.surfaces.sets()); segment_sets.extend(c.surfaces.sets());
} }
for t in model.ties().values() {
segment_sets.extend([t.main.as_str(), t.secondary.as_str()]);
}
let mut gravity_sets: BTreeSet<Option<&str>> = BTreeSet::new(); let mut gravity_sets: BTreeSet<Option<&str>> = BTreeSet::new();
for step in model.steps() { for step in model.steps() {
if matches!(step.procedure, Procedure::ExplicitDynamic(_)) { if matches!(step.procedure, Procedure::ExplicitDynamic(_)) {
@@ -733,6 +737,21 @@ fn contacts(d: &mut Deck, model: &Model, plan: &Plan) -> Result<()> {
d.header(&["sfsa", "sfsb"]); d.header(&["sfsa", "sfsb"]);
d.card([Field::from(c.penalty_scale), Field::from(c.penalty_scale)]); d.card([Field::from(c.penalty_scale), Field::from(c.penalty_scale)]);
} }
// Ties: a tied surface-to-surface contact between the segment sets, secondary first.
for (name, t) in model.ties() {
let sid = |set: &str| plan.sets.get(set).copied().ok_or_else(|| broken(format!("tie `{name}`: set `{set}`")));
d.comment(&format!("tie `{name}`"));
if t.position_tolerance.is_some() {
d.comment("FieldCraft's position tolerance: LS-DYNA ties the nodes within its own search distance");
}
d.line("*CONTACT_TIED_SURFACE_TO_SURFACE");
d.header(&["surfa", "surfb", "surfatyp", "surfbtyp"]);
d.card([Field::from(sid(&t.secondary)?), Field::from(sid(&t.main)?), Field::from(0), Field::from(0)]);
d.header(&["fs", "fd", "dc"]);
d.card([Field::from(0.0), Field::from(0.0), Field::from(0.0)]);
d.header(&["sfsa", "sfsb"]);
d.card([Field::from(1.0), Field::from(1.0)]);
}
Ok(()) Ok(())
} }
+3
View File
@@ -337,6 +337,9 @@ impl<'m> Deck<'m> {
for name in self.model.contacts().keys() { for name in self.model.contacts().keys() {
self.problems.insert(format!("contact `{name}` (Nastran's linear solutions SOL 101 and 103 have no contact)")); self.problems.insert(format!("contact `{name}` (Nastran's linear solutions SOL 101 and 103 have no contact)"));
} }
for name in self.model.ties().keys() {
self.problems.insert(format!("tie `{name}` (this writer does not write glued contact yet)"));
}
for sub in &mut self.subcases { for sub in &mut self.subcases {
if matches!(sub.step.procedure, Procedure::Frequency { .. }) { if matches!(sub.step.procedure, Procedure::Frequency { .. }) {
sub.method = Some(next_id(&mut self.next_sid)?); sub.method = Some(next_id(&mut self.next_sid)?);
+5 -1
View File
@@ -4,7 +4,7 @@
use fieldcraft_decks::{Error, abaqus, lsdyna, nastran}; use fieldcraft_decks::{Error, abaqus, lsdyna, nastran};
use fieldcraft_model::{ use fieldcraft_model::{
BoundaryCondition, Contact, ContactSurfaces, Elastic, ElementFace, ElementKind, Friction, InitialPenetration, Material, Model, Procedure, BoundaryCondition, Contact, ContactSurfaces, Elastic, ElementFace, ElementKind, Friction, InitialPenetration, Material, Model, Procedure,
Section, SectionKind, Set, Step, Target, UnitSystem, Section, SectionKind, Set, Step, Target, Tie, UnitSystem,
}; };
/// Two unit hex8 cubes, the second on top of the first (separate nodes), with face sets for the /// Two unit hex8 cubes, the second on top of the first (separate nodes), with face sets for the
@@ -42,6 +42,7 @@ fn model() -> Model {
m.add_contact("stack", pair).unwrap(); m.add_contact("stack", pair).unwrap();
m.add_contact("everything", Contact { friction: Friction::coulomb(0.1), ..Contact::new(ContactSurfaces::General { surface: None }) }).unwrap(); m.add_contact("everything", Contact { friction: Friction::coulomb(0.1), ..Contact::new(ContactSurfaces::General { surface: None }) }).unwrap();
m.add_contact("fold", Contact { penalty_scale: 0.5, ..Contact::new(ContactSurfaces::General { surface: Some("upper".into()) }) }).unwrap(); m.add_contact("fold", Contact { penalty_scale: 0.5, ..Contact::new(ContactSurfaces::General { surface: Some("upper".into()) }) }).unwrap();
m.add_tie("glue", Tie { position_tolerance: Some(0.05), ..Tie::new("lower", "upper") }).unwrap();
m m
} }
@@ -56,6 +57,7 @@ fn abaqus_writes_interactions_pairs_and_general_contact() {
"*SURFACE INTERACTION, NAME=fold-interaction\n*FRICTION\n0.0\n", "*SURFACE INTERACTION, NAME=fold-interaction\n*FRICTION\n0.0\n",
"*CONTACT\n*CONTACT INCLUSIONS, ALL EXTERIOR\n*CONTACT INCLUSIONS\nupper,\n*CONTACT PROPERTY ASSIGNMENT\n, , everything-interaction\nupper, upper, fold-interaction\n", "*CONTACT\n*CONTACT INCLUSIONS, ALL EXTERIOR\n*CONTACT INCLUSIONS\nupper,\n*CONTACT PROPERTY ASSIGNMENT\n, , everything-interaction\nupper, upper, fold-interaction\n",
"** contact `fold`: penalty scale 0.5", "** contact `fold`: penalty scale 0.5",
"*TIE, NAME=glue, ADJUST=NO, POSITION TOLERANCE=0.05\nupper, lower\n",
] { ] {
assert!(deck.contains(expected), "{expected}\n{deck}"); assert!(deck.contains(expected), "{expected}\n{deck}");
} }
@@ -83,6 +85,7 @@ fn lsdyna_writes_automatic_contacts_on_segment_sets() {
for set in ["lower", "upper"] { for set in ["lower", "upper"] {
assert!(deck.contains(&format!("*SET_SEGMENT_TITLE\n{set}\n")), "{set}\n{deck}"); assert!(deck.contains(&format!("*SET_SEGMENT_TITLE\n{set}\n")), "{set}\n{deck}");
} }
assert_eq!(after("*CONTACT_TIED_SURFACE_TO_SURFACE"), ["4 3 0 0", "0.0 0.0 0.0", "1.0 1.0"], "{deck}");
} }
#[test] #[test]
@@ -92,6 +95,7 @@ fn nastran_refuses_contact_by_name() {
for name in ["everything", "fold", "stack"] { for name in ["everything", "fold", "stack"] {
assert!(what.contains(&format!("contact `{name}`")), "{what}"); assert!(what.contains(&format!("contact `{name}`")), "{what}");
} }
assert!(what.contains("tie `glue`"), "{what}");
} }
other => panic!("{other:?}"), other => panic!("{other:?}"),
} }