forked from 1b-agent/fieldcraft-upstream
model: contact pairs, general contact and Coulomb friction, mapped by the deck writers
Contact definitions on face sets: a pair (each surface's nodes against the other's faces), general contact on a surface (self contact) or on every exterior face of the solids; Coulomb friction with static and kinetic coefficients and an exponential decay, a penalty scale and the choice to adjust or refuse initially penetrating nodes, validated with caps. Rigid walls of explicit steps gain friction. The check wants solid faces on contact surfaces; sets used by a contact cannot be deleted. fem::contact: the closest point of a triangle (Ericson) or a bilinear quadrilateral (projected Newton) to a point, and a bucket grid of face boxes for the global search, deterministic and bounded. Commands interaction.contact.pair, .general, .edit, .delete and .find (the touching parts of the mesh, with face sets and pairs made of them); contacts in the model browser. Abaqus writes *SURFACE INTERACTION with *FRICTION (exponential decay), *CONTACT PAIR and general *CONTACT; LS-DYNA *CONTACT_AUTOMATIC_SURFACE_TO_SURFACE and _SINGLE_SURFACE with FS, FD, DC; Nastran refuses contact by name. The implicit solver refuses contact by name (M7); the explicit solver refuses it until the next commit. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
6c2b2509e7
commit
ecf947c090
23 files changed
+1494
-29
No files matched your search
@@ -13,8 +13,20 @@
|
||||
//!
|
||||
//! In order: heading, nodes (mesh nodes, then reference points), elements grouped by element type,
|
||||
//! sets (a node set, an element set and an element-face surface per model set, each written when
|
||||
//! the set resolves to some), materials, sections, couplings, the initial state's boundary
|
||||
//! conditions, then the steps.
|
||||
//! the set resolves to some), materials, sections, couplings, contact, the initial state's
|
||||
//! boundary conditions, then the steps.
|
||||
//!
|
||||
//! # Contact
|
||||
//!
|
||||
//! Each contact is a `*SURFACE INTERACTION` with its friction (`*FRICTION`, or
|
||||
//! `*FRICTION, EXPONENTIAL DECAY` with the static and kinetic coefficients and the decay
|
||||
//! coefficient, the same law). A pair is `*CONTACT PAIR, TYPE=SURFACE TO SURFACE` with the
|
||||
//! secondary surface first, and `ADJUST=0.0` when initially overclosed nodes are adjusted. General
|
||||
//! contact is the model's one `*CONTACT` definition: `*CONTACT INCLUSIONS, ALL EXTERIOR` for every
|
||||
//! exterior face, a surface with a blank partner for self contact, and a
|
||||
//! `*CONTACT PROPERTY ASSIGNMENT` line per contact. FieldCraft's penalty scale and its refusal of
|
||||
//! initial overclosure have no Abaqus counterpart and are noted in comments (Abaqus chooses its
|
||||
//! own penalty and resolves overclosure in the first increment).
|
||||
//!
|
||||
//! # Steps and the activity table
|
||||
//!
|
||||
@@ -130,8 +142,8 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use fieldcraft_model::{
|
||||
BeamProfile, BoundaryCondition, CouplingKind, Elastic, Element, ElementKind, Hardening, Hyperelastic, Integration, Load, Model, OutputVariable,
|
||||
Plastic, Procedure, Resolved, Section, SectionKind, Step, Symmetry, Target,
|
||||
BeamProfile, BoundaryCondition, ContactSurfaces, CouplingKind, Elastic, Element, ElementKind, Hardening, Hyperelastic, InitialPenetration,
|
||||
Integration, Load, Model, OutputVariable, Plastic, Procedure, Resolved, Section, SectionKind, Step, Symmetry, Target,
|
||||
};
|
||||
|
||||
use crate::{Error, Result, unsupported};
|
||||
@@ -174,6 +186,7 @@ pub fn write_resolved(model: &Model, r: &Resolved) -> Result<String> {
|
||||
d.sections(model)?;
|
||||
d.discrete_properties(&discrete)?;
|
||||
d.couplings(model, r);
|
||||
d.contacts(model, r)?;
|
||||
d.steps(model, r)?;
|
||||
Ok(d.out)
|
||||
}
|
||||
@@ -957,6 +970,78 @@ impl Deck {
|
||||
}
|
||||
}
|
||||
|
||||
/// One surface interaction (the friction) per contact, then its pair, and every general
|
||||
/// contact in the model's one `*CONTACT` definition.
|
||||
fn contacts(&mut self, model: &Model, r: &Resolved) -> Result<()> {
|
||||
if model.contacts().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut taken: BTreeSet<String> = model.sets().keys().map(|n| n.to_ascii_uppercase()).collect();
|
||||
taken.extend(coupling_surfaces(model, r).into_values().map(|n| n.to_ascii_uppercase()));
|
||||
self.comment("Contact: a surface interaction (Coulomb friction) per contact, contact pairs, general contact");
|
||||
let mut general = Vec::new();
|
||||
for (name, c) in model.contacts() {
|
||||
let interaction = crate::unique_name(&format!("{name}-interaction"), &mut taken);
|
||||
self.line(&format!("*SURFACE INTERACTION, NAME={interaction}"));
|
||||
let f = c.friction;
|
||||
if f.decay > 0.0 {
|
||||
self.line("*FRICTION, EXPONENTIAL DECAY");
|
||||
self.numbers_line("", &[f.static_coefficient, f.kinetic_coefficient(), f.decay])?;
|
||||
} else {
|
||||
self.line("*FRICTION");
|
||||
self.numbers_line("", &[f.static_coefficient])?;
|
||||
}
|
||||
if c.penalty_scale != 1.0 {
|
||||
self.comment(&format!(
|
||||
"contact `{name}`: penalty scale {} of FieldCraft's explicit penalty stiffness; Abaqus chooses its own",
|
||||
c.penalty_scale
|
||||
));
|
||||
}
|
||||
match &c.surfaces {
|
||||
ContactSurfaces::Pair { main, secondary } => {
|
||||
// ADJUST=0.0 moves the secondary nodes that start inside the main surface
|
||||
// onto it, strain-free; Abaqus has no refusal of initial overclosure.
|
||||
let adjust = match c.initial_penetration {
|
||||
InitialPenetration::Adjust => ", ADJUST=0.0",
|
||||
InitialPenetration::Refuse => {
|
||||
self.comment(&format!(
|
||||
"contact `{name}`: FieldCraft refuses initially overclosed nodes; Abaqus resolves them in the first increment"
|
||||
));
|
||||
""
|
||||
}
|
||||
};
|
||||
self.line(&format!("*CONTACT PAIR, INTERACTION={interaction}, TYPE=SURFACE TO SURFACE{adjust}"));
|
||||
self.line(&format!("{secondary}, {main}"));
|
||||
}
|
||||
ContactSurfaces::General { surface } => general.push((surface.clone(), interaction)),
|
||||
}
|
||||
}
|
||||
if general.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
// General contact: every surface against itself (a blank second surface), or every
|
||||
// exterior face; each with its interaction.
|
||||
self.line("*CONTACT");
|
||||
if general.iter().any(|(s, _)| s.is_none()) {
|
||||
self.line("*CONTACT INCLUSIONS, ALL EXTERIOR");
|
||||
}
|
||||
let named: Vec<&str> = general.iter().filter_map(|(s, _)| s.as_deref()).collect();
|
||||
if !named.is_empty() {
|
||||
self.line("*CONTACT INCLUSIONS");
|
||||
for s in &named {
|
||||
self.line(&format!("{s},"));
|
||||
}
|
||||
}
|
||||
self.line("*CONTACT PROPERTY ASSIGNMENT");
|
||||
for (surface, interaction) in &general {
|
||||
match surface {
|
||||
Some(s) => self.line(&format!("{s}, {s}, {interaction}")),
|
||||
None => self.line(&format!(", , {interaction}")),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn boundary_lines<'n>(&mut self, model: &Model, r: &Resolved, names: impl IntoIterator<Item = &'n String>) -> Result<()> {
|
||||
for name in names {
|
||||
let Some(bc) = model.boundary_condition(name) else { continue };
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
//! - couplings: kinematic as `*CONSTRAINED_NODAL_RIGID_BODY` (the reference point first in its
|
||||
//! 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
|
||||
//! 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
|
||||
//! that start inside the other surface out, as `adjust` does; a refusal is noted in a comment;
|
||||
//! - per step: `*CONTROL_IMPLICIT_GENERAL`, `*CONTROL_IMPLICIT_SOLUTION` (linear) or
|
||||
//! `*CONTROL_IMPLICIT_EIGENVALUE`, `*CONTROL_TERMINATION` (a static step is one implicit step
|
||||
//! from time 0 to 1 with every load ramped by one curve, `*DEFINE_CURVE`); a nonlinear static
|
||||
@@ -69,8 +75,8 @@ mod elements;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use fieldcraft_model::{
|
||||
BoundaryCondition, CouplingKind, Diagnostic, Dofs, Elastic, Element, ElementKind, Hardening, Load, Material, Model, OutputVariable, Plastic,
|
||||
Procedure, Resolved, ResolvedDirection, Section, SectionKind, Step, Target,
|
||||
BoundaryCondition, ContactSurfaces, CouplingKind, Diagnostic, Dofs, Elastic, Element, ElementKind, Hardening, InitialPenetration, Load, Material,
|
||||
Model, OutputVariable, Plastic, Procedure, Resolved, ResolvedDirection, Section, SectionKind, Step, Target,
|
||||
};
|
||||
|
||||
use crate::{Error, Result};
|
||||
@@ -177,6 +183,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)?;
|
||||
contacts(&mut d, model, &plan)?;
|
||||
discrete(&mut d, model, r, &plan)?;
|
||||
if let Some(parts) = &plan.gravity_parts {
|
||||
d.line("*SET_PART_LIST");
|
||||
@@ -269,6 +276,10 @@ fn plan<'a>(model: &'a Model, r: &Resolved) -> Result<Plan<'a>> {
|
||||
node_sets.insert(c.set.as_str());
|
||||
}
|
||||
}
|
||||
// Contact surfaces are segment sets.
|
||||
for c in model.contacts().values() {
|
||||
segment_sets.extend(c.surfaces.sets());
|
||||
}
|
||||
let mut gravity_sets: BTreeSet<Option<&str>> = BTreeSet::new();
|
||||
for step in model.steps() {
|
||||
if matches!(step.procedure, Procedure::ExplicitDynamic(_)) {
|
||||
@@ -689,6 +700,42 @@ fn couplings(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan) -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Contacts (see the module docs): a pair as an automatic surface-to-surface contact between the
|
||||
/// segment sets, general contact as an automatic single-surface contact.
|
||||
fn contacts(d: &mut Deck, model: &Model, plan: &Plan) -> Result<()> {
|
||||
for (name, c) in model.contacts() {
|
||||
let sid = |set: &str| plan.sets.get(set).copied().ok_or_else(|| broken(format!("contact `{name}`: set `{set}`")));
|
||||
d.comment(&format!("contact `{name}`"));
|
||||
if c.initial_penetration == InitialPenetration::Refuse {
|
||||
d.comment("FieldCraft refuses nodes that start inside the other surface; LS-DYNA moves them out (as `adjust` does)");
|
||||
}
|
||||
match &c.surfaces {
|
||||
ContactSurfaces::Pair { main, secondary } => {
|
||||
d.line("*CONTACT_AUTOMATIC_SURFACE_TO_SURFACE");
|
||||
d.header(&["surfa", "surfb", "surfatyp", "surfbtyp"]);
|
||||
d.card([Field::from(sid(secondary)?), Field::from(sid(main)?), Field::from(0), Field::from(0)]);
|
||||
}
|
||||
ContactSurfaces::General { surface: Some(s) } => {
|
||||
d.line("*CONTACT_AUTOMATIC_SINGLE_SURFACE");
|
||||
d.header(&["surfa", "surfb", "surfatyp"]);
|
||||
d.card([Field::from(sid(s)?), Field::Blank, Field::from(0)]);
|
||||
}
|
||||
ContactSurfaces::General { surface: None } => {
|
||||
// SURFATYP 5: every part.
|
||||
d.line("*CONTACT_AUTOMATIC_SINGLE_SURFACE");
|
||||
d.header(&["surfa", "surfb", "surfatyp"]);
|
||||
d.card([Field::from(0), Field::Blank, Field::from(5)]);
|
||||
}
|
||||
}
|
||||
let f = c.friction;
|
||||
d.header(&["fs", "fd", "dc"]);
|
||||
d.card([Field::from(f.static_coefficient), Field::from(f.kinetic_coefficient()), Field::from(f.decay)]);
|
||||
d.header(&["sfsa", "sfsb"]);
|
||||
d.card([Field::from(c.penalty_scale), Field::from(c.penalty_scale)]);
|
||||
}
|
||||
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() {
|
||||
|
||||
@@ -331,6 +331,9 @@ impl<'m> Deck<'m> {
|
||||
self.plan_constraints()?;
|
||||
self.plan_loads()?;
|
||||
self.problems.extend(crate::thermal_items(self.model));
|
||||
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 sub in &mut self.subcases {
|
||||
if matches!(sub.step.procedure, Procedure::Frequency { .. }) {
|
||||
sub.method = Some(next_id(&mut self.next_sid)?);
|
||||
|
||||
Reference in new issue
Block a user