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 <noreply@anthropic.com> 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
@@ -0,0 +1,98 @@
|
||||
//! 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:?}"),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user