diff --git a/crates/decks/src/abaqus.rs b/crates/decks/src/abaqus.rs index e48fafb..010be25 100644 --- a/crates/decks/src/abaqus.rs +++ b/crates/decks/src/abaqus.rs @@ -28,6 +28,11 @@ //! initial overclosure have no Abaqus counterpart and are noted in comments (Abaqus chooses its //! 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 //! //! 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 { d.discrete_properties(&discrete)?; d.couplings(model, r); d.contacts(model, r)?; + d.ties(model)?; d.steps(model, r)?; Ok(d.out) } @@ -1042,6 +1048,20 @@ impl Deck { 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) -> Result<()> { for name in names { let Some(bc) = model.boundary_condition(name) else { continue }; diff --git a/crates/decks/src/lsdyna/mod.rs b/crates/decks/src/lsdyna/mod.rs index c2516f7..929da99 100644 --- a/crates/decks/src/lsdyna/mod.rs +++ b/crates/decks/src/lsdyna/mod.rs @@ -23,7 +23,8 @@ //! node set), distributing as `*CONSTRAINED_INTERPOLATION` (the reference point dependent on //! the set's translations, equal weights); //! - 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 //! 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 @@ -280,6 +281,9 @@ fn plan<'a>(model: &'a Model, r: &Resolved) -> Result> { for c in model.contacts().values() { 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> = BTreeSet::new(); for step in model.steps() { 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.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(()) } diff --git a/crates/decks/src/nastran/mod.rs b/crates/decks/src/nastran/mod.rs index 7669618..f807129 100644 --- a/crates/decks/src/nastran/mod.rs +++ b/crates/decks/src/nastran/mod.rs @@ -337,6 +337,9 @@ impl<'m> Deck<'m> { for name in self.model.contacts().keys() { 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 { if matches!(sub.step.procedure, Procedure::Frequency { .. }) { sub.method = Some(next_id(&mut self.next_sid)?); diff --git a/crates/decks/tests/contact.rs b/crates/decks/tests/contact.rs index 1021ff7..6a99acb 100644 --- a/crates/decks/tests/contact.rs +++ b/crates/decks/tests/contact.rs @@ -4,7 +4,7 @@ use fieldcraft_decks::{Error, abaqus, lsdyna, nastran}; use fieldcraft_model::{ 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 @@ -42,6 +42,7 @@ fn model() -> Model { 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("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 } @@ -56,6 +57,7 @@ fn abaqus_writes_interactions_pairs_and_general_contact() { "*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 `fold`: penalty scale 0.5", + "*TIE, NAME=glue, ADJUST=NO, POSITION TOLERANCE=0.05\nupper, lower\n", ] { assert!(deck.contains(expected), "{expected}\n{deck}"); } @@ -83,6 +85,7 @@ fn lsdyna_writes_automatic_contacts_on_segment_sets() { for set in ["lower", "upper"] { 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] @@ -92,6 +95,7 @@ fn nastran_refuses_contact_by_name() { for name in ["everything", "fold", "stack"] { assert!(what.contains(&format!("contact `{name}`")), "{what}"); } + assert!(what.contains("tie `glue`"), "{what}"); } other => panic!("{other:?}"), }