forked from 1b-agent/fieldcraft-upstream
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:
1 parent
a91ac5baf9
commit
8d2fa980a5
4 files changed
+139
-1
No files matched your search
@@ -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
|
// 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).
|
// (worked out only when a boundary condition or load needs it).
|
||||||
let rotational = std::cell::OnceCell::new();
|
let rotational = std::cell::OnceCell::new();
|
||||||
|
|||||||
+12
-1
@@ -45,6 +45,7 @@ pub mod nodal;
|
|||||||
pub mod sections;
|
pub mod sections;
|
||||||
pub mod sets;
|
pub mod sets;
|
||||||
pub mod steps;
|
pub mod steps;
|
||||||
|
pub mod tie;
|
||||||
pub mod units;
|
pub mod units;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
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,
|
MAX_LOAD_PATH_POINTS, MAX_NONLINEAR_INCREMENTS, MAX_NONLINEAR_ITERATIONS, MassScaling, NonlinearStatic, OutputVariable, Procedure, RigidWall,
|
||||||
Step, StressRate, Tolerances, Transient,
|
Step, StressRate, Tolerances, Transient,
|
||||||
};
|
};
|
||||||
|
pub use tie::Tie;
|
||||||
pub use units::UnitSystem;
|
pub use units::UnitSystem;
|
||||||
|
|
||||||
/// The largest node or element label. Nastran's GRID and element ids stop at 99 999 999 (eight
|
/// 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")]
|
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||||
contacts: BTreeMap<String, Contact>,
|
contacts: BTreeMap<String, Contact>,
|
||||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||||
|
ties: BTreeMap<String, Tie>,
|
||||||
|
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||||
masses: BTreeMap<String, PointMass>,
|
masses: BTreeMap<String, PointMass>,
|
||||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||||
springs: BTreeMap<String, Spring>,
|
springs: BTreeMap<String, Spring>,
|
||||||
@@ -164,6 +168,8 @@ struct ModelData {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
contacts: BTreeMap<String, Contact>,
|
contacts: BTreeMap<String, Contact>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
ties: BTreeMap<String, Tie>,
|
||||||
|
#[serde(default)]
|
||||||
masses: BTreeMap<String, PointMass>,
|
masses: BTreeMap<String, PointMass>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
springs: BTreeMap<String, Spring>,
|
springs: BTreeMap<String, Spring>,
|
||||||
@@ -225,6 +231,9 @@ impl TryFrom<ModelData> for Model {
|
|||||||
for (n, v) in d.contacts {
|
for (n, v) in d.contacts {
|
||||||
m.add_contact(&n, v)?;
|
m.add_contact(&n, v)?;
|
||||||
}
|
}
|
||||||
|
for (n, v) in d.ties {
|
||||||
|
m.add_tie(&n, v)?;
|
||||||
|
}
|
||||||
for (n, v) in d.masses {
|
for (n, v) in d.masses {
|
||||||
m.add_mass(&n, v)?;
|
m.add_mass(&n, v)?;
|
||||||
}
|
}
|
||||||
@@ -341,6 +350,7 @@ impl Model {
|
|||||||
reference_points: self.reference_points.clone(),
|
reference_points: self.reference_points.clone(),
|
||||||
couplings: self.couplings.clone(),
|
couplings: self.couplings.clone(),
|
||||||
contacts: self.contacts.clone(),
|
contacts: self.contacts.clone(),
|
||||||
|
ties: self.ties.clone(),
|
||||||
masses: self.masses.clone(),
|
masses: self.masses.clone(),
|
||||||
springs: self.springs.clone(),
|
springs: self.springs.clone(),
|
||||||
boundary_conditions: self.boundary_conditions.clone(),
|
boundary_conditions: self.boundary_conditions.clone(),
|
||||||
@@ -351,7 +361,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Counts of each kind of definition, for summaries.
|
/// 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()),
|
("sets", self.sets.len()),
|
||||||
("materials", self.materials.len()),
|
("materials", self.materials.len()),
|
||||||
@@ -359,6 +369,7 @@ impl Model {
|
|||||||
("reference_points", self.reference_points.len()),
|
("reference_points", self.reference_points.len()),
|
||||||
("couplings", self.couplings.len()),
|
("couplings", self.couplings.len()),
|
||||||
("contacts", self.contacts.len()),
|
("contacts", self.contacts.len()),
|
||||||
|
("ties", self.ties.len()),
|
||||||
("masses", self.masses.len()),
|
("masses", self.masses.len()),
|
||||||
("springs", self.springs.len()),
|
("springs", self.springs.len()),
|
||||||
("boundary_conditions", self.boundary_conditions.len()),
|
("boundary_conditions", self.boundary_conditions.len()),
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ impl Model {
|
|||||||
users.extend(self.sections.iter().filter(|(_, s)| s.set == set).map(|(n, _)| format!("section `{n}`")));
|
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.couplings.iter().filter(|(_, c)| c.set == set).map(|(n, _)| format!("coupling `{n}`")));
|
||||||
users.extend(self.contact_users(set));
|
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.discrete_users(|t| t.set() == Some(set)));
|
||||||
users
|
users
|
||||||
.extend(self.boundary_conditions.iter().filter(|(_, b)| b.target().set() == Some(set)).map(|(n, _)| format!("boundary condition `{n}`")));
|
.extend(self.boundary_conditions.iter().filter(|(_, b)| b.target().set() == Some(set)).map(|(n, _)| format!("boundary condition `{n}`")));
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user