model: ties between surfaces

A tie names a main and a secondary face set and an optional position tolerance; it refers to
its sets, round-trips, is bounded in number and checked for solid faces.

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 a91ac5baf9
commit 8d2fa980a5
4 files changed
+139 -1

No files matched your search

+16
View File
@@ -404,6 +404,22 @@ impl Model {
}
}
// Ties: their surfaces are element faces of solids too.
for (name, t) in &self.ties {
for set in [&t.main, &t.secondary] {
let faces = &set_of(set).faces;
let solid = |f: &crate::ElementFace| self.elements.get(&f.element).is_some_and(|e| e.kind.dimension() == 3);
if faces.is_empty() {
d.error(format!("tie `{name}`"), format!("set `{set}` has no element faces (a tie surface is a set of faces)"));
} else if !faces.iter().all(solid) {
d.error(
format!("tie `{name}`"),
format!("set `{set}` has faces of elements that are not solids (ties bond the faces of solid elements so far)"),
);
}
}
}
// Nodes with rotational degrees of freedom: those of shells and beams, and reference points
// (worked out only when a boundary condition or load needs it).
let rotational = std::cell::OnceCell::new();
+12 -1
View File
@@ -45,6 +45,7 @@ pub mod nodal;
pub mod sections;
pub mod sets;
pub mod steps;
pub mod tie;
pub mod units;
use std::collections::BTreeMap;
@@ -71,6 +72,7 @@ pub use steps::{
MAX_LOAD_PATH_POINTS, MAX_NONLINEAR_INCREMENTS, MAX_NONLINEAR_ITERATIONS, MassScaling, NonlinearStatic, OutputVariable, Procedure, RigidWall,
Step, StressRate, Tolerances, Transient,
};
pub use tie::Tie;
pub use units::UnitSystem;
/// The largest node or element label. Nastran's GRID and element ids stop at 99 999 999 (eight
@@ -128,6 +130,8 @@ pub struct Model {
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
contacts: BTreeMap<String, Contact>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
ties: BTreeMap<String, Tie>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
masses: BTreeMap<String, PointMass>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
springs: BTreeMap<String, Spring>,
@@ -164,6 +168,8 @@ struct ModelData {
#[serde(default)]
contacts: BTreeMap<String, Contact>,
#[serde(default)]
ties: BTreeMap<String, Tie>,
#[serde(default)]
masses: BTreeMap<String, PointMass>,
#[serde(default)]
springs: BTreeMap<String, Spring>,
@@ -225,6 +231,9 @@ impl TryFrom<ModelData> for Model {
for (n, v) in d.contacts {
m.add_contact(&n, v)?;
}
for (n, v) in d.ties {
m.add_tie(&n, v)?;
}
for (n, v) in d.masses {
m.add_mass(&n, v)?;
}
@@ -341,6 +350,7 @@ impl Model {
reference_points: self.reference_points.clone(),
couplings: self.couplings.clone(),
contacts: self.contacts.clone(),
ties: self.ties.clone(),
masses: self.masses.clone(),
springs: self.springs.clone(),
boundary_conditions: self.boundary_conditions.clone(),
@@ -351,7 +361,7 @@ impl Model {
}
/// Counts of each kind of definition, for summaries.
pub fn definition_counts(&self) -> [(&'static str, usize); 12] {
pub fn definition_counts(&self) -> [(&'static str, usize); 13] {
[
("sets", self.sets.len()),
("materials", self.materials.len()),
@@ -359,6 +369,7 @@ impl Model {
("reference_points", self.reference_points.len()),
("couplings", self.couplings.len()),
("contacts", self.contacts.len()),
("ties", self.ties.len()),
("masses", self.masses.len()),
("springs", self.springs.len()),
("boundary_conditions", self.boundary_conditions.len()),
+1
View File
@@ -123,6 +123,7 @@ impl Model {
users.extend(self.sections.iter().filter(|(_, s)| s.set == set).map(|(n, _)| format!("section `{n}`")));
users.extend(self.couplings.iter().filter(|(_, c)| c.set == set).map(|(n, _)| format!("coupling `{n}`")));
users.extend(self.contact_users(set));
users.extend(self.tie_users(set));
users.extend(self.discrete_users(|t| t.set() == Some(set)));
users
.extend(self.boundary_conditions.iter().filter(|(_, b)| b.target().set() == Some(set)).map(|(n, _)| format!("boundary condition `{n}`")));
+110
View File
@@ -0,0 +1,110 @@
//! Tie: two surfaces of non-matching meshes bonded together.
//!
//! A tie names a `main` and a `secondary` surface, sets of element faces. The solvers make every
//! node of the secondary surface follow the main surface under it (the implicit solver by mortar
//! constraints with dual shape functions, so a constant stress crosses the tie exactly). Nodes of
//! the secondary surface farther than the `position_tolerance` from the main surface stay free
//! (by default half the thickness of the main element under them).
use serde::{Deserialize, Serialize};
use crate::{Error, Model, Result, context, insert_new, remove_existing, replace_existing, require};
/// Most ties in a model.
pub const MAX_TIES: usize = 1000;
/// A tie definition.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Tie {
pub main: String,
pub secondary: String,
/// How far a secondary node may lie from the main surface and still be tied (`None`: half
/// the thickness of the main element under it).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub position_tolerance: Option<f64>,
}
impl Tie {
pub fn new(main: &str, secondary: &str) -> Tie {
Tie { main: main.to_string(), secondary: secondary.to_string(), position_tolerance: None }
}
pub fn validate(&self) -> Result<()> {
if self.main == self.secondary {
return Err(Error::Invalid(format!("a tie needs two different surfaces, got `{}` twice", self.main)));
}
if let Some(t) = self.position_tolerance
&& !(t > 0.0 && t.is_finite())
{
return Err(Error::Invalid(format!("a tie's position tolerance is a positive length, got {t}")));
}
Ok(())
}
}
impl Model {
pub fn ties(&self) -> &std::collections::BTreeMap<String, Tie> {
&self.ties
}
pub fn tie(&self, name: &str) -> Option<&Tie> {
self.ties.get(name)
}
fn check_tie(&self, name: &str, t: &Tie) -> Result<()> {
t.validate().map_err(|e| context("tie", name, e))?;
for set in [&t.main, &t.secondary] {
require(&self.sets, "set", set).map_err(|e| context("tie", name, e))?;
}
Ok(())
}
pub fn add_tie(&mut self, name: &str, tie: Tie) -> Result<()> {
self.check_tie(name, &tie)?;
if self.ties.len() >= MAX_TIES {
return Err(Error::Limit(format!("a model holds at most {MAX_TIES} ties")));
}
insert_new(&mut self.ties, "tie", name, tie)
}
pub fn replace_tie(&mut self, name: &str, tie: Tie) -> Result<()> {
self.check_tie(name, &tie)?;
replace_existing(&mut self.ties, "tie", name, tie)
}
pub fn remove_tie(&mut self, name: &str) -> Result<()> {
remove_existing(&mut self.ties, "tie", name).map(|_| ())
}
/// The ties that name a set, as ``tie `name` `` strings.
pub(crate) fn tie_users<'a>(&'a self, set: &'a str) -> impl Iterator<Item = String> + 'a {
self.ties.iter().filter(move |(_, t)| t.main == set || t.secondary == set).map(|(n, _)| format!("tie `{n}`"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Set;
#[test]
fn ties_validate_refer_to_sets_and_round_trip() {
let mut m = Model::default();
for s in ["a", "b"] {
m.add_set(s, Set { nodes: vec![1], ..Set::default() }).unwrap();
}
m.add_tie("glue", Tie::new("a", "b")).unwrap();
assert!(matches!(m.remove_set("a"), Err(Error::InUse(_))), "the tie refers to the set");
assert!(m.add_tie("glue", Tie::new("a", "b")).is_err(), "names stay unique");
assert!(m.add_tie("self", Tie::new("a", "a")).is_err());
assert!(m.add_tie("far", Tie { position_tolerance: Some(-1.0), ..Tie::new("a", "b") }).is_err());
assert!(matches!(m.add_tie("missing", Tie::new("a", "c")), Err(Error::NotFound(_))));
let text = serde_json::to_string(&m).unwrap();
let back: Model = serde_json::from_str(&text).unwrap();
assert_eq!(back.ties(), m.ties());
m.replace_tie("glue", Tie { position_tolerance: Some(0.1), ..Tie::new("b", "a") }).unwrap();
m.remove_tie("glue").unwrap();
assert!(m.remove_tie("glue").is_err());
}
}