diff --git a/crates/model/src/check.rs b/crates/model/src/check.rs index effdaff..f4ca60f 100644 --- a/crates/model/src/check.rs +++ b/crates/model/src/check.rs @@ -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(); diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index 4c9c21f..f24c165 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -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, #[serde(skip_serializing_if = "BTreeMap::is_empty")] + ties: BTreeMap, + #[serde(skip_serializing_if = "BTreeMap::is_empty")] masses: BTreeMap, #[serde(skip_serializing_if = "BTreeMap::is_empty")] springs: BTreeMap, @@ -164,6 +168,8 @@ struct ModelData { #[serde(default)] contacts: BTreeMap, #[serde(default)] + ties: BTreeMap, + #[serde(default)] masses: BTreeMap, #[serde(default)] springs: BTreeMap, @@ -225,6 +231,9 @@ impl TryFrom 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()), diff --git a/crates/model/src/sets.rs b/crates/model/src/sets.rs index 84964c7..fc94301 100644 --- a/crates/model/src/sets.rs +++ b/crates/model/src/sets.rs @@ -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}`"))); diff --git a/crates/model/src/tie.rs b/crates/model/src/tie.rs new file mode 100644 index 0000000..b1143ad --- /dev/null +++ b/crates/model/src/tie.rs @@ -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, +} + +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 { + &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 + '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()); + } +}