//! The solver-neutral finite element model. //! //! A [`Model`] holds what every solver deck describes, in no solver's vocabulary: //! - the mesh: nodes and elements addressed by the positive integer labels the decks use //! ([`mesh`]), and how the mesh lies on the geometry ([`geometry`]: each element, element face, //! element edge and node tagged with the persistent name of the body, face, edge or vertex it //! was meshed on); //! - named sets ([`sets`]) defined by geometry names or explicit labels, resolved to nodes, //! elements and element faces against the current mesh, so they survive remeshing; //! - materials and the unit system ([`materials`], [`units`]), sections with their element //! formulation ([`sections`]), reference points and couplings ([`constraints`]), point masses, //! springs and dashpots ([`discrete`]); //! - boundary conditions ([`bcs`]), loads ([`loads`]) and steps with an explicit table of which //! boundary conditions and loads are active in each step ([`steps`]); //! - the check that names every missing or bad item, and the resolved view the deck writers and //! the solver read ([`check`]). //! //! Solver formats (Abaqus input, Nastran bulk data, LS-DYNA keyword) translate to and from this //! model; nothing here knows any of them. //! //! The model is unit-consistent: it stores numbers in whatever consistent system the user chose //! (for example mm, tonne, s, N, MPa), never converts them, and only warns when a magnitude looks //! wrong for the chosen system. //! //! Every mutation validates its input, because labels, names and numbers come from files, scripts //! and the control channel: a bad value is an [`Error`], never a panic. Definitions that refer to //! other definitions (a section to its set and material, a load to its set) must refer to ones //! that exist, and a definition still referred to cannot be deleted. What depends on the mesh (a //! set's geometry names and labels) is checked when the model is resolved, because remeshing //! replaces the mesh under the definitions. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![forbid(unsafe_code)] pub mod bcs; pub mod check; pub mod constraints; pub mod contact; pub mod discrete; pub mod fluids; pub mod geometry; pub mod loads; pub mod materials; mod mesh; pub mod nodal; pub mod sections; pub mod sets; pub mod steps; pub mod tie; pub mod units; use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; pub use bcs::{BoundaryCondition, Dofs, Symmetry, TEMPERATURE_DOF, Target}; pub use check::{Diagnostic, Resolved, ResolvedSet, Severity}; pub use constraints::{Coupling, CouplingKind, ReferencePoint}; pub use contact::{Contact, ContactSurfaces, Friction, InitialPenetration}; pub use discrete::{Direction, Mass, PointMass, ResolvedDirection, ResolvedMass, ResolvedSpring, Spring}; pub use fluids::{BoxFace, BoxFaces, FaceCondition, FluidBoundary, FluidDomain, Profile, SurfaceBoundary, SurfaceCondition}; pub use geometry::{Classification, Entity, GeometryRef}; pub use loads::{Distribution, Load, centrifugal_acceleration}; pub use materials::{Damage, Directional, Elastic, Erosion, Fluid, Hardening, HardeningPoint, Hyperelastic, Material, Plastic, Thermal}; pub use mesh::{ Element, ElementEdge, ElementFace, ElementKind, MIN_NORMALIZED_JACOBIAN, Node, admissible_jacobian, check_coordinates, normalized_jacobian, tet4_jacobian, }; pub use sections::{BeamProfile, Extent, Formulation, Integration, Section, SectionKind}; pub use sets::Set; pub use steps::{ ArcConstraint, ArcLength, BulkViscosity, DisplacementLimit, ExplicitDynamic, Hourglass, HourglassKind, Imperfection, InitialVelocity, 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 /// digits in a small field), the tightest of the three formats. pub const MAX_LABEL: u32 = 99_999_999; /// Caps on model size, so hostile input cannot exhaust memory. pub const MAX_NODES: usize = 50_000_000; pub const MAX_ELEMENTS: usize = 50_000_000; /// Coordinates beyond this magnitude are rejected (1e9 model units). pub const MAX_COORD: f64 = 1e9; /// Most definitions of one kind (sets, materials, sections, …) a model holds. pub const MAX_DEFINITIONS: usize = 100_000; /// Longest name of a definition. Names are ASCII: a letter, then letters, digits, `_` or `-`, so /// every deck format can carry them unquoted. pub const MAX_NAME_LEN: usize = 64; #[derive(Debug, Clone, PartialEq, thiserror::Error)] pub enum Error { #[error("invalid input: {0}")] Invalid(String), #[error("not found: {0}")] NotFound(String), #[error("limit reached: {0}")] Limit(String), /// A definition cannot be deleted while another refers to it. #[error("in use: {0}")] InUse(String), } pub type Result = std::result::Result; /// The finite element model. /// /// It deserializes by replaying what it reads through the same checked methods commands use, so /// a model read from a file holds the same invariants as one built by commands. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(try_from = "ModelData")] pub struct Model { #[serde(skip_serializing_if = "BTreeMap::is_empty")] nodes: BTreeMap, #[serde(skip_serializing_if = "BTreeMap::is_empty")] elements: BTreeMap, #[serde(skip_serializing_if = "Classification::is_unused")] classification: Classification, units: UnitSystem, sets: BTreeMap, materials: BTreeMap, sections: BTreeMap, reference_points: BTreeMap, couplings: BTreeMap, #[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, boundary_conditions: BTreeMap, loads: BTreeMap, #[serde(skip_serializing_if = "BTreeMap::is_empty")] fluid_domains: BTreeMap, /// In analysis order. steps: Vec, } /// A [`Model`] as stored, before it is checked. #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct ModelData { #[serde(default)] nodes: BTreeMap, #[serde(default)] elements: BTreeMap, #[serde(default)] classification: geometry::ClassificationData, #[serde(default)] units: UnitSystem, #[serde(default)] sets: BTreeMap, #[serde(default)] materials: BTreeMap, #[serde(default)] sections: BTreeMap, #[serde(default)] reference_points: BTreeMap, #[serde(default)] couplings: BTreeMap, #[serde(default)] contacts: BTreeMap, #[serde(default)] ties: BTreeMap, #[serde(default)] masses: BTreeMap, #[serde(default)] springs: BTreeMap, #[serde(default)] boundary_conditions: BTreeMap, #[serde(default)] loads: BTreeMap, #[serde(default)] fluid_domains: BTreeMap, #[serde(default)] steps: Vec, } impl TryFrom for Model { type Error = Error; fn try_from(d: ModelData) -> Result { let mut m = Model { units: d.units, ..Model::default() }; for (label, node) in d.nodes { m.set_node(label, node.xyz)?; } for (label, e) in d.elements { m.set_element(label, e.kind, e.nodes)?; } for (i, entity) in d.classification.entities.iter().enumerate() { if usize::try_from(m.geometry_tag(entity)?).ok() != Some(i) { return Err(Error::Invalid(format!("{entity} is listed twice among the geometry tags"))); } } for (element, tags) in d.classification.elements { if let Some(t) = tags.region { m.tag_element(element, t)?; } for (face, t) in tags.faces { m.tag_element_face(element, face, t)?; } for (edge, t) in tags.edges { m.tag_element_edge(element, edge, t)?; } } for (node, t) in d.classification.nodes { m.tag_node(node, t)?; } for (n, v) in d.sets { m.add_set(&n, v)?; } for (n, v) in d.materials { m.add_material(&n, v)?; } for (n, v) in d.sections { m.add_section(&n, v)?; } for (n, v) in d.reference_points { m.add_reference_point(&n, v)?; } for (n, v) in d.couplings { m.add_coupling(&n, v)?; } 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)?; } for (n, v) in d.springs { m.add_spring(&n, v)?; } for (n, v) in d.boundary_conditions { m.add_boundary_condition(&n, v)?; } for (n, v) in d.loads { m.add_load(&n, v)?; } for (n, v) in d.fluid_domains { m.add_fluid_domain(&n, v)?; } for s in d.steps { m.add_step(s, None)?; } Ok(m) } } /// Check a definition's name: 1..=[`MAX_NAME_LEN`] ASCII characters, a letter first, then /// letters, digits, `_` or `-`. pub fn check_name(what: &str, name: &str) -> Result<()> { let mut chars = name.chars(); let ok = name.len() <= MAX_NAME_LEN && chars.next().is_some_and(|c| c.is_ascii_alphabetic()) && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'); if ok { Ok(()) } else { let shown: String = name.chars().take(MAX_NAME_LEN).collect(); Err(Error::Invalid(format!("{what} name `{shown}` must be 1 to {MAX_NAME_LEN} characters: a letter, then letters, digits, `_` or `-`"))) } } /// Check that a value is a finite number, naming it in the error. pub(crate) fn finite(what: &str, v: f64) -> Result { if v.is_finite() { Ok(v) } else { Err(Error::Invalid(format!("{what} must be a finite number, got {v}"))) } } /// Check that a value is a finite number above zero, naming it in the error. pub(crate) fn positive(what: &str, v: f64) -> Result { if v > 0.0 && v.is_finite() { Ok(v) } else { Err(Error::Invalid(format!("{what} must be a positive number, got {v}"))) } } /// Add a new definition under a fresh, valid name. fn insert_new(map: &mut BTreeMap, what: &str, name: &str, value: T) -> Result<()> { check_name(what, name)?; if map.contains_key(name) { return Err(Error::Invalid(format!("{what} `{name}` already exists"))); } if map.len() >= MAX_DEFINITIONS { return Err(Error::Limit(format!("a model holds at most {MAX_DEFINITIONS} of each kind of definition"))); } map.insert(name.to_string(), value); Ok(()) } /// Replace an existing definition. fn replace_existing(map: &mut BTreeMap, what: &str, name: &str, value: T) -> Result<()> { let slot = map.get_mut(name).ok_or_else(|| Error::NotFound(format!("{what} `{name}`")))?; *slot = value; Ok(()) } /// Prefix an error with the definition it is about. pub(crate) fn context(what: &str, name: &str, e: Error) -> Error { let shown: String = name.chars().take(MAX_NAME_LEN).collect(); match e { Error::Invalid(m) => Error::Invalid(format!("{what} `{shown}`: {m}")), Error::NotFound(m) => Error::NotFound(format!("{what} `{shown}`: {m}")), Error::Limit(m) => Error::Limit(format!("{what} `{shown}`: {m}")), Error::InUse(m) => Error::InUse(format!("{what} `{shown}`: {m}")), } } /// Remove an existing definition. fn remove_existing(map: &mut BTreeMap, what: &str, name: &str) -> Result { map.remove(name).ok_or_else(|| Error::NotFound(format!("{what} `{name}`"))) } /// Fail with [`Error::InUse`] when any referrer is listed. fn not_in_use(what: &str, name: &str, referrers: Vec) -> Result<()> { if referrers.is_empty() { Ok(()) } else { Err(Error::InUse(format!("{what} `{name}` is used by {}", referrers.join(", ")))) } } fn require(map: &BTreeMap, what: &str, name: &str) -> Result<()> { if map.contains_key(name) { Ok(()) } else { Err(Error::NotFound(format!("{what} `{name}`"))) } } impl Model { pub fn units(&self) -> UnitSystem { self.units } /// Label the unit system the model's numbers are in (nothing is converted). pub fn set_units(&mut self, units: UnitSystem) { self.units = units; } /// A copy with the unit system and every definition but no mesh: no nodes, elements or /// geometry tags. A `.fieldcraft` file stores this part as text and the mesh in binary. pub fn without_mesh(&self) -> Model { Model { nodes: BTreeMap::new(), elements: BTreeMap::new(), classification: Classification::default(), units: self.units, sets: self.sets.clone(), materials: self.materials.clone(), sections: self.sections.clone(), 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(), loads: self.loads.clone(), fluid_domains: self.fluid_domains.clone(), steps: self.steps.clone(), } } /// Counts of each kind of definition, for summaries. pub fn definition_counts(&self) -> [(&'static str, usize); 13] { [ ("sets", self.sets.len()), ("materials", self.materials.len()), ("sections", self.sections.len()), ("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()), ("loads", self.loads.len()), ("fluid_domains", self.fluid_domains.len()), ("steps", self.steps.len()), ] } }