Model definition: sets by geometry name, materials, sections, couplings, BCs, loads, steps, check (#16)

* model: split nodes and elements into a mesh module (no behaviour change)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* model: sets by geometry name, materials and units, sections, couplings, BCs, loads, steps and the check

Sets bind to persistent geometry names through the mesh's geometry tags
(element, element face/edge and node classification) and resolve against
the current mesh, so they survive a remesh. Steps list their active loads
and boundary conditions explicitly. Model::check names every missing or
bad item; Model::resolve gives the writers' view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* engine: model definition commands (sets, materials, sections, RPs, couplings, BCs, loads, steps, tags, check)

Each definition has create/edit/delete with its model JSON form as
parameters; model.step.activate and model.activity drive the explicit step
table; mesh.tag records where the mesh lies on the geometry; model.check
and model.set.resolve report against the current mesh. Hostile params run
against an empty and a fully defined session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* docs: model definition in architecture and roadmap

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* model, engine: review cleanups

Validated deserialization (a file replays through the checked methods),
interned geometry tags by index, set resolution with tag bitsets and sorted
vectors, one delete policy (a load or BC listed by a step is in use),
shared helpers (context, remove_existing, step_mut, opt/req/name/listed),
dead code removed, Resolved without a per-element section map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* model: mesh-independent force distribution, tractions, stable reference point labels

Force and moment on a set default to a total, split by consistent nodal
weights over the set's faces (or equally over a node set); per_node is an
explicit opt-in. Traction is force per area. resolve() expands them to
nodal values for every writer and the solver. Reference points keep a
stored node label taken from the top of the label range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 16:19:54 -04:00
1 parent 6c36f5b2ad
commit 57b4b2f4d2
23 files changed
+4296 -268

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
//! Loads. Which steps a load is active in is listed explicitly by each step ([`crate::steps`]).
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::bcs::Target;
use crate::{Error, Model, Result, context, finite, insert_new, not_in_use, remove_existing, replace_existing, require};
/// How a force or moment on a set is shared among the set's nodes.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Distribution {
/// The vector is the total. On a set with element faces it is split by the faces' consistent
/// nodal weights (area-weighted, [`Model::surface_weights`]); on a set of nodes only, equally.
/// The total is the same on every mesh of the set.
#[default]
Total,
/// The vector is applied at every node of the set, so the total grows with the number of
/// nodes: it depends on the mesh.
PerNode,
}
/// A load.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum Load {
/// A force on the target's nodes, shared as `distribution` says (a reference point takes it
/// whole).
Force {
target: Target,
vector: [f64; 3],
#[serde(default)]
distribution: Distribution,
},
/// A moment on the target's nodes, shared as `distribution` says. Solid elements have no
/// rotations, so a moment on a solid goes through a coupled reference point.
Moment {
target: Target,
vector: [f64; 3],
#[serde(default)]
distribution: Distribution,
},
/// A force per area on the set's element faces, as consistent nodal forces.
Traction { set: String, vector: [f64; 3] },
/// Uniform pressure on the set's element faces, positive pushing into the elements (against
/// the outward normal).
Pressure { set: String, magnitude: f64 },
/// Gravity: an acceleration of the mass of the set's elements, or of the whole model when no
/// set is given.
Gravity {
acceleration: [f64; 3],
#[serde(default, skip_serializing_if = "Option::is_none")]
set: Option<String>,
},
/// A force per volume on the set's elements.
BodyForce { set: String, vector: [f64; 3] },
}
impl Load {
/// `force`, `moment`, `traction`, `pressure`, `gravity` or `body_force`.
pub fn kind(&self) -> &'static str {
match self {
Load::Force { .. } => "force",
Load::Moment { .. } => "moment",
Load::Traction { .. } => "traction",
Load::Pressure { .. } => "pressure",
Load::Gravity { .. } => "gravity",
Load::BodyForce { .. } => "body_force",
}
}
/// The set the load acts on, if any.
pub fn set(&self) -> Option<&str> {
match self {
Load::Force { target, .. } | Load::Moment { target, .. } => target.set(),
Load::Pressure { set, .. } | Load::BodyForce { set, .. } | Load::Traction { set, .. } => Some(set),
Load::Gravity { set, .. } => set.as_deref(),
}
}
/// The reference point the load acts on, if any.
pub fn reference_point(&self) -> Option<&str> {
match self {
Load::Force { target, .. } | Load::Moment { target, .. } => target.reference_point(),
_ => None,
}
}
pub fn validate(&self) -> Result<()> {
let vector = match self {
Load::Force { vector, .. } | Load::Moment { vector, .. } | Load::BodyForce { vector, .. } | Load::Traction { vector, .. } => vector,
Load::Gravity { acceleration, .. } => acceleration,
Load::Pressure { magnitude, .. } => {
finite("pressure", *magnitude)?;
return Ok(());
}
};
for c in vector {
finite(self.kind(), *c)?;
}
if vector.iter().all(|c| *c == 0.0) {
return Err(Error::Invalid(format!("a {} vector must not be zero", self.kind())));
}
Ok(())
}
}
impl Model {
pub fn loads(&self) -> &BTreeMap<String, Load> {
&self.loads
}
pub fn load(&self, name: &str) -> Option<&Load> {
self.loads.get(name)
}
fn check_load(&self, name: &str, load: &Load) -> Result<()> {
load.validate().map_err(|e| context("load", name, e))?;
if let Some(s) = load.set() {
require(&self.sets, "set", s).map_err(|e| context("load", name, e))?;
}
if let Some(r) = load.reference_point() {
require(&self.reference_points, "reference point", r).map_err(|e| context("load", name, e))?;
}
Ok(())
}
pub fn add_load(&mut self, name: &str, load: Load) -> Result<()> {
self.check_load(name, &load)?;
insert_new(&mut self.loads, "load", name, load)
}
pub fn replace_load(&mut self, name: &str, load: Load) -> Result<()> {
self.check_load(name, &load)?;
replace_existing(&mut self.loads, "load", name, load)
}
/// Remove a load that no step lists.
pub fn remove_load(&mut self, name: &str) -> Result<()> {
let users = self.steps.iter().filter(|s| s.loads.contains(name)).map(|s| format!("step `{}`", s.name)).collect();
not_in_use("load", name, users)?;
remove_existing(&mut self.loads, "load", name).map(|_| ())
}
}