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 @@
|
||||
//! Reference points and couplings.
|
||||
//!
|
||||
//! A reference point is a single node off the mesh with three translations and three rotations.
|
||||
//! A coupling ties it to a set's nodes, so a force, moment or boundary condition on the point acts
|
||||
//! on a whole face (a bolt hole, a mounting pad) without hand-picked nodes.
|
||||
//!
|
||||
//! Each reference point keeps the node label it is written under in every deck. Unless one is
|
||||
//! given, it gets the highest free label at the top of the label range when it is created, far
|
||||
//! above the labels meshers use (they count up from 1), so remeshing, adding nodes and adding or
|
||||
//! removing other reference points never renumber it. A mesh node on the same label is an error
|
||||
//! of the check.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::bcs::Dofs;
|
||||
use crate::{Error, MAX_LABEL, Model, Result, check_coordinates, context, insert_new, not_in_use, remove_existing, replace_existing, require};
|
||||
|
||||
/// A reference point.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ReferencePoint {
|
||||
pub xyz: [f64; 3],
|
||||
/// Its node label in decks; 0 when creating means "choose one" (see the module docs).
|
||||
#[serde(default)]
|
||||
pub label: u32,
|
||||
}
|
||||
|
||||
/// How a coupling ties the reference point to the set's nodes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CouplingKind {
|
||||
/// The nodes follow the reference point rigidly in the coupled degrees of freedom.
|
||||
Kinematic,
|
||||
/// The reference point's load is spread over the nodes (weighted equally), and its motion is
|
||||
/// their weighted average; the region stays free to deform.
|
||||
Distributing,
|
||||
}
|
||||
|
||||
/// A coupling between a reference point and a set's nodes.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Coupling {
|
||||
pub reference_point: String,
|
||||
pub set: String,
|
||||
pub kind: CouplingKind,
|
||||
/// The coupled degrees of freedom (all six by default).
|
||||
#[serde(default = "Dofs::all")]
|
||||
pub dofs: Dofs,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn reference_points(&self) -> &BTreeMap<String, ReferencePoint> {
|
||||
&self.reference_points
|
||||
}
|
||||
|
||||
pub fn reference_point(&self, name: &str) -> Option<&ReferencePoint> {
|
||||
self.reference_points.get(name)
|
||||
}
|
||||
|
||||
/// Check a reference point's coordinates and label, choosing a label when it has none.
|
||||
fn place_reference_point(&self, name: &str, mut point: ReferencePoint, current: Option<u32>) -> Result<ReferencePoint> {
|
||||
check_coordinates(&point.xyz).map_err(|e| context("reference point", name, e))?;
|
||||
let taken = |l: u32| self.reference_points.iter().any(|(n, p)| n != name && p.label == l);
|
||||
if point.label == 0 {
|
||||
point.label = match current {
|
||||
Some(l) => l,
|
||||
None => (1..=MAX_LABEL)
|
||||
.rev()
|
||||
.take(crate::MAX_DEFINITIONS + 1)
|
||||
.find(|l| !taken(*l))
|
||||
.ok_or_else(|| Error::Limit(format!("no node label left for reference point `{name}`")))?,
|
||||
};
|
||||
}
|
||||
if point.label > MAX_LABEL {
|
||||
return Err(context("reference point", name, Error::Invalid(format!("label {} is outside 1..={MAX_LABEL}", point.label))));
|
||||
}
|
||||
if let Some((other, _)) = self.reference_points.iter().find(|(n, p)| *n != name && p.label == point.label) {
|
||||
return Err(context("reference point", name, Error::Invalid(format!("label {} is taken by reference point `{other}`", point.label))));
|
||||
}
|
||||
Ok(point)
|
||||
}
|
||||
|
||||
pub fn add_reference_point(&mut self, name: &str, point: ReferencePoint) -> Result<()> {
|
||||
let point = self.place_reference_point(name, point, None)?;
|
||||
insert_new(&mut self.reference_points, "reference point", name, point)
|
||||
}
|
||||
|
||||
/// Replace a reference point; a label of 0 keeps its current one.
|
||||
pub fn replace_reference_point(&mut self, name: &str, point: ReferencePoint) -> Result<()> {
|
||||
let current = self.reference_points.get(name).map(|p| p.label).ok_or_else(|| Error::NotFound(format!("reference point `{name}`")))?;
|
||||
let point = self.place_reference_point(name, point, Some(current))?;
|
||||
replace_existing(&mut self.reference_points, "reference point", name, point)
|
||||
}
|
||||
|
||||
pub fn remove_reference_point(&mut self, name: &str) -> Result<()> {
|
||||
let mut users: Vec<String> =
|
||||
self.couplings.iter().filter(|(_, c)| c.reference_point == name).map(|(n, _)| format!("coupling `{n}`")).collect();
|
||||
users.extend(
|
||||
self.boundary_conditions
|
||||
.iter()
|
||||
.filter(|(_, b)| b.target().reference_point() == Some(name))
|
||||
.map(|(n, _)| format!("boundary condition `{n}`")),
|
||||
);
|
||||
users.extend(self.loads.iter().filter(|(_, l)| l.reference_point() == Some(name)).map(|(n, _)| format!("load `{n}`")));
|
||||
not_in_use("reference point", name, users)?;
|
||||
remove_existing(&mut self.reference_points, "reference point", name).map(|_| ())
|
||||
}
|
||||
|
||||
/// The node label of each reference point in decks.
|
||||
pub fn reference_point_nodes(&self) -> BTreeMap<String, u32> {
|
||||
self.reference_points.iter().map(|(n, p)| (n.clone(), p.label)).collect()
|
||||
}
|
||||
|
||||
pub fn couplings(&self) -> &BTreeMap<String, Coupling> {
|
||||
&self.couplings
|
||||
}
|
||||
|
||||
pub fn coupling(&self, name: &str) -> Option<&Coupling> {
|
||||
self.couplings.get(name)
|
||||
}
|
||||
|
||||
fn check_coupling(&self, name: &str, c: &Coupling) -> Result<()> {
|
||||
if c.dofs.is_empty() {
|
||||
return Err(context("coupling", name, Error::Invalid("couples no degree of freedom".into())));
|
||||
}
|
||||
require(&self.reference_points, "reference point", &c.reference_point).map_err(|e| context("coupling", name, e))?;
|
||||
require(&self.sets, "set", &c.set).map_err(|e| context("coupling", name, e))
|
||||
}
|
||||
|
||||
pub fn add_coupling(&mut self, name: &str, coupling: Coupling) -> Result<()> {
|
||||
self.check_coupling(name, &coupling)?;
|
||||
insert_new(&mut self.couplings, "coupling", name, coupling)
|
||||
}
|
||||
|
||||
pub fn replace_coupling(&mut self, name: &str, coupling: Coupling) -> Result<()> {
|
||||
self.check_coupling(name, &coupling)?;
|
||||
replace_existing(&mut self.couplings, "coupling", name, coupling)
|
||||
}
|
||||
|
||||
pub fn remove_coupling(&mut self, name: &str) -> Result<()> {
|
||||
remove_existing(&mut self.couplings, "coupling", name).map(|_| ())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user