forked from 1b-agent/fieldcraft-upstream
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
99 lines
5.2 KiB
Rust
99 lines
5.2 KiB
Rust
//! Contact definitions in every deck format: Abaqus surface interactions, contact pairs and
|
|
//! general contact; LS-DYNA automatic contacts; Nastran refuses them by name.
|
|
|
|
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,
|
|
};
|
|
|
|
/// Two unit hex8 cubes, the second on top of the first (separate nodes), with face sets for the
|
|
/// touching faces and a static step.
|
|
fn model() -> Model {
|
|
let mut m = Model::default();
|
|
m.set_units(UnitSystem::MmTonneS);
|
|
let corners = [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.], [0., 0., 1.], [1., 0., 1.], [1., 1., 1.], [0., 1., 1.]];
|
|
for cube in 0..2u32 {
|
|
for (i, c) in corners.iter().enumerate() {
|
|
m.set_node(8 * cube + i as u32 + 1, [c[0], c[1], c[2] + cube as f64]).unwrap();
|
|
}
|
|
m.set_element(cube + 1, ElementKind::Hex8, (8 * cube + 1..=8 * cube + 8).collect()).unwrap();
|
|
}
|
|
m.add_set("all", Set { elements: vec![1, 2], ..Set::default() }).unwrap();
|
|
m.add_set("lower", Set { faces: vec![ElementFace { element: 1, face: 2 }], ..Set::default() }).unwrap();
|
|
m.add_set("upper", Set { faces: vec![ElementFace { element: 2, face: 1 }], ..Set::default() }).unwrap();
|
|
m.add_set("base", Set { nodes: vec![1, 2, 3, 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("all", Section { set: "all".into(), material: "Steel".into(), kind: SectionKind::Solid {}, formulation: Default::default() })
|
|
.unwrap();
|
|
m.add_boundary_condition("fix", BoundaryCondition::Fixed { target: Target::Set("base".into()) }).unwrap();
|
|
let mut step = Step::new("Load", Procedure::Static {});
|
|
step.boundary_conditions.insert("fix".into());
|
|
m.add_step(step, None).unwrap();
|
|
let pair = Contact {
|
|
friction: Friction { static_coefficient: 0.3, kinetic: Some(0.2), decay: 5.0 },
|
|
initial_penetration: InitialPenetration::Adjust,
|
|
..Contact::new(ContactSurfaces::Pair { main: "lower".into(), secondary: "upper".into() })
|
|
};
|
|
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
|
|
}
|
|
|
|
#[test]
|
|
fn abaqus_writes_interactions_pairs_and_general_contact() {
|
|
let deck = abaqus::write(&model()).unwrap();
|
|
for expected in [
|
|
"*SURFACE, TYPE=ELEMENT, NAME=lower\n1, S2\n",
|
|
"*SURFACE INTERACTION, NAME=stack-interaction\n*FRICTION, EXPONENTIAL DECAY\n0.3, 0.2, 5.0\n",
|
|
"*CONTACT PAIR, INTERACTION=stack-interaction, TYPE=SURFACE TO SURFACE, ADJUST=0.0\nupper, lower\n",
|
|
"*SURFACE INTERACTION, NAME=everything-interaction\n*FRICTION\n0.1\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 `fold`: penalty scale 0.5",
|
|
] {
|
|
assert!(deck.contains(expected), "{expected}\n{deck}");
|
|
}
|
|
// Model data: before the first step.
|
|
assert!(deck.find("*CONTACT PAIR") < deck.find("*STEP"));
|
|
}
|
|
|
|
#[test]
|
|
fn lsdyna_writes_automatic_contacts_on_segment_sets() {
|
|
let deck = lsdyna::write(&model()).unwrap().text;
|
|
let after = |keyword: &str| -> Vec<String> {
|
|
let at = deck.find(keyword).unwrap_or_else(|| panic!("{keyword}\n{deck}"));
|
|
deck[at..].lines().skip(1).filter(|l| !l.starts_with('$')).take(3).map(|l| l.split_whitespace().collect::<Vec<_>>().join(" ")).collect()
|
|
};
|
|
// Sets are numbered in name order: all 1, base 2, lower 3, upper 4.
|
|
assert_eq!(after("*CONTACT_AUTOMATIC_SURFACE_TO_SURFACE"), ["4 3 0 0", "0.3 0.2 5.0", "1.0 1.0"], "{deck}");
|
|
let single: Vec<&str> = deck.match_indices("*CONTACT_AUTOMATIC_SINGLE_SURFACE").map(|(i, _)| &deck[i..]).collect();
|
|
assert_eq!(single.len(), 2, "{deck}");
|
|
let cards = |text: &str| -> Vec<String> {
|
|
text.lines().skip(1).filter(|l| !l.starts_with('$')).take(3).map(|l| l.split_whitespace().collect::<Vec<_>>().join(" ")).collect()
|
|
};
|
|
// `everything` (every part), then `fold` (the upper face's segment set).
|
|
assert_eq!(cards(single[0]), ["0 5", "0.1 0.1 0.0", "1.0 1.0"], "{deck}");
|
|
assert_eq!(cards(single[1]), ["4 0", "0.0 0.0 0.0", "0.5 0.5"], "{deck}");
|
|
for set in ["lower", "upper"] {
|
|
assert!(deck.contains(&format!("*SET_SEGMENT_TITLE\n{set}\n")), "{set}\n{deck}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nastran_refuses_contact_by_name() {
|
|
match nastran::write(&model()) {
|
|
Err(Error::Unsupported { what, .. }) => {
|
|
for name in ["everything", "fold", "stack"] {
|
|
assert!(what.contains(&format!("contact `{name}`")), "{what}");
|
|
}
|
|
}
|
|
other => panic!("{other:?}"),
|
|
}
|
|
}
|