forked from 1b-agent/fieldcraft-upstream
* 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>
147 lines
6.0 KiB
Rust
147 lines
6.0 KiB
Rust
//! The unit system label.
|
||
//!
|
||
//! The model never converts: numbers are stored as entered. The label records which consistent
|
||
//! system they are in, so the check can warn when a magnitude is implausible for it (a steel
|
||
//! modulus of 210 000 in a metre–pascal model, a density of 7850 in a millimetre–tonne one), and
|
||
//! so writers can say which system a deck is in.
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
/// A consistent unit system: length, mass, time, force, stress.
|
||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||
pub enum UnitSystem {
|
||
/// Not chosen: magnitudes are not checked.
|
||
#[default]
|
||
#[serde(rename = "unspecified")]
|
||
Unspecified,
|
||
/// Metre, kilogram, second, newton, pascal.
|
||
#[serde(rename = "m-kg-s-N-Pa")]
|
||
MKgS,
|
||
/// Millimetre, tonne, second, newton, megapascal: the usual choice for parts in millimetres.
|
||
#[serde(rename = "mm-t-s-N-MPa")]
|
||
MmTonneS,
|
||
/// Millimetre, kilogram, millisecond, kilonewton, gigapascal.
|
||
#[serde(rename = "mm-kg-ms-kN-GPa")]
|
||
MmKgMs,
|
||
/// Millimetre, gram, millisecond, newton, megapascal.
|
||
#[serde(rename = "mm-g-ms-N-MPa")]
|
||
MmGMs,
|
||
/// Inch, pound-force second squared per inch, second, pound-force, psi.
|
||
#[serde(rename = "in-lbf-s-psi")]
|
||
InLbfS,
|
||
}
|
||
|
||
impl UnitSystem {
|
||
pub const ALL: [UnitSystem; 6] =
|
||
[UnitSystem::Unspecified, UnitSystem::MKgS, UnitSystem::MmTonneS, UnitSystem::MmKgMs, UnitSystem::MmGMs, UnitSystem::InLbfS];
|
||
|
||
/// The label used in commands and files (`mm-t-s-N-MPa`, …).
|
||
pub fn label(self) -> &'static str {
|
||
match self {
|
||
UnitSystem::Unspecified => "unspecified",
|
||
UnitSystem::MKgS => "m-kg-s-N-Pa",
|
||
UnitSystem::MmTonneS => "mm-t-s-N-MPa",
|
||
UnitSystem::MmKgMs => "mm-kg-ms-kN-GPa",
|
||
UnitSystem::MmGMs => "mm-g-ms-N-MPa",
|
||
UnitSystem::InLbfS => "in-lbf-s-psi",
|
||
}
|
||
}
|
||
|
||
pub fn from_label(label: &str) -> Option<UnitSystem> {
|
||
UnitSystem::ALL.into_iter().find(|u| u.label() == label)
|
||
}
|
||
|
||
/// The length unit (`m`, `mm`, `in`), or `None` when unspecified.
|
||
pub fn length(self) -> Option<&'static str> {
|
||
match self {
|
||
UnitSystem::Unspecified => None,
|
||
UnitSystem::MKgS => Some("m"),
|
||
UnitSystem::MmTonneS | UnitSystem::MmKgMs | UnitSystem::MmGMs => Some("mm"),
|
||
UnitSystem::InLbfS => Some("in"),
|
||
}
|
||
}
|
||
|
||
/// Size of one stress unit in pascals, one density unit in kg/m³ and one acceleration unit in
|
||
/// m/s², or `None` when unspecified.
|
||
fn scales(self) -> Option<Scales> {
|
||
const IN: f64 = 0.0254;
|
||
const LBF: f64 = 4.448_221_615_260_5;
|
||
Some(match self {
|
||
UnitSystem::Unspecified => return None,
|
||
UnitSystem::MKgS => Scales { stress: 1.0, density: 1.0, acceleration: 1.0 },
|
||
UnitSystem::MmTonneS => Scales { stress: 1e6, density: 1e12, acceleration: 1e-3 },
|
||
UnitSystem::MmKgMs => Scales { stress: 1e9, density: 1e9, acceleration: 1e3 },
|
||
UnitSystem::MmGMs => Scales { stress: 1e6, density: 1e6, acceleration: 1e3 },
|
||
// Mass unit lbf·s²/in = LBF / IN kg; density unit = that per cubic inch.
|
||
UnitSystem::InLbfS => Scales { stress: LBF / (IN * IN), density: LBF / IN / (IN * IN * IN), acceleration: IN },
|
||
})
|
||
}
|
||
|
||
/// A warning when a Young's or shear modulus is implausible for any engineering material in
|
||
/// this system (outside 10 kPa – 2 TPa).
|
||
pub fn check_modulus(self, what: &str, value: f64) -> Option<String> {
|
||
self.check(what, value, |s| s.stress, (1e4, 2e12), "Pa")
|
||
}
|
||
|
||
/// A warning when a mass density is implausible (outside 1 – 25 000 kg/m³).
|
||
pub fn check_density(self, value: f64) -> Option<String> {
|
||
self.check("density", value, |s| s.density, (1.0, 25_000.0), "kg/m³")
|
||
}
|
||
|
||
/// A warning when a gravitational acceleration is implausible (outside 0.1 – 100 m/s²).
|
||
pub fn check_gravity(self, magnitude: f64) -> Option<String> {
|
||
self.check("gravity", magnitude, |s| s.acceleration, (0.1, 100.0), "m/s²")
|
||
}
|
||
|
||
fn check(self, what: &str, value: f64, scale: fn(&Scales) -> f64, (lo, hi): (f64, f64), si: &str) -> Option<String> {
|
||
let s = self.scales()?;
|
||
let si_value = value.abs() * scale(&s);
|
||
if (lo..=hi).contains(&si_value) {
|
||
return None;
|
||
}
|
||
let k = scale(&s);
|
||
Some(format!(
|
||
"{what} {value} is {si_value:.3e} {si}, outside the plausible {lo:.0e}–{hi:.0e} {si} ({:.3e}–{:.3e} in {}); check the unit system",
|
||
lo / k,
|
||
hi / k,
|
||
self.label()
|
||
))
|
||
}
|
||
}
|
||
|
||
struct Scales {
|
||
stress: f64,
|
||
density: f64,
|
||
acceleration: f64,
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn steel_is_plausible_in_every_system_and_wrong_systems_warn() {
|
||
// Steel: E = 210 GPa, ρ = 7850 kg/m³, g = 9.81 m/s², in each system's own units.
|
||
let steel = [
|
||
(UnitSystem::MKgS, 2.1e11, 7850.0, 9.81),
|
||
(UnitSystem::MmTonneS, 2.1e5, 7.85e-9, 9810.0),
|
||
(UnitSystem::MmKgMs, 210.0, 7.85e-6, 9.81e-3),
|
||
(UnitSystem::MmGMs, 2.1e5, 7.85e-3, 9.81e-3),
|
||
(UnitSystem::InLbfS, 3.05e7, 7.35e-4, 386.1),
|
||
];
|
||
for (u, e, rho, g) in steel {
|
||
assert_eq!(u.check_modulus("E", e), None, "{u:?}");
|
||
assert_eq!(u.check_density(rho), None, "{u:?}");
|
||
assert_eq!(u.check_gravity(g), None, "{u:?}");
|
||
assert_eq!(UnitSystem::from_label(u.label()), Some(u));
|
||
assert_eq!(serde_json::to_value(u).unwrap(), serde_json::json!(u.label()));
|
||
}
|
||
// SI numbers in a millimetre–tonne model.
|
||
assert!(UnitSystem::MmTonneS.check_modulus("E", 2.1e11).is_some());
|
||
assert!(UnitSystem::MmTonneS.check_density(7850.0).is_some());
|
||
assert!(UnitSystem::MKgS.check_gravity(9810.0).is_some());
|
||
assert_eq!(UnitSystem::Unspecified.check_density(1e30), None);
|
||
assert!(UnitSystem::MKgS.check_density(f64::NAN).is_some());
|
||
}
|
||
}
|