Files
OneCAD-Simulation/crates/model/src/constraints.rs
T
jyvikoandClaude Opus 5.5 0bf150633c Model: datum points, drawn as a small cross; each definition's users are public
Datum::Point joins planes and axes (checked like them, refused by
partitions with the kind named). The users each remove_* refuses with
(sets, materials, boundary conditions, loads, reference points, fluid
domains, datums) are public functions, so a caller can list them before
deleting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:51:01 -04:00

152 lines
6.7 KiB
Rust

//! 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<()> {
not_in_use("reference point", name, self.reference_point_users(name))?;
remove_existing(&mut self.reference_points, "reference point", name).map(|_| ())
}
/// The definitions that refer to a reference point, as `kind \`name\`` strings.
pub fn reference_point_users(&self, name: &str) -> Vec<String> {
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}`")));
users.extend(self.discrete_users(|t| t.reference_point() == Some(name)));
users
}
/// 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(|_| ())
}
}