forked from 1b-agent/fieldcraft-upstream
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:
23 files changed
+4296
-268
No files matched your search
@@ -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(|_| ())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user