forked from 1b-agent/fieldcraft-upstream
Element quality metrics and the certified validity gate (mesh.quality) (#12)
* mesh: element quality metrics and a certified Jacobian validity check Bernstein-form Jacobian determinants for tet4/tet10/wedge6/hex8/hex20, certified by bounds and subdivision (Johnen-Remacle-Geuzaine 2013); Shewchuk 2002 tet metrics, Knupp 2001 hex/wedge/quad metrics; quality report and the validity gate meshers run on their output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * engine: mesh.quality command; model documents its node order mesh.quality reports certified validity, volume and per-metric statistics with fixed-range histograms. The model's ElementKind doc states the Abaqus/VTK node order and that deck code reorders formats like Nastran CHEXA20. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * quality: simplify after review; exact orient3d for tet4; gate on raw mesher arrays - tet4 validity from the exact orient3d of the input coordinates - validity_gate_elements takes (label, kind, coordinates) straight from a mesher - one Jacobian per solid; certify caches piece bounds; product weights precomputed - model::check_coordinates shared by set_node and quality; ElementKind is Ord - engine: shared labels() parameter parser 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:
15 files changed
+1981
-17
No files matched your search
+24
-6
@@ -47,9 +47,17 @@ pub struct Node {
|
||||
pub xyz: [f64; 3],
|
||||
}
|
||||
|
||||
/// Element topology. Node order follows each family's usual corner-first convention (corners,
|
||||
/// then mid-side nodes), which all three target formats share for these shapes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
/// Element topology. Node order is corners first, then mid-side nodes, with positive volume for
|
||||
/// a right-handed ordering:
|
||||
/// - tet10: mid-side nodes 5–10 on edges 1-2, 2-3, 3-1, 1-4, 2-4, 3-4;
|
||||
/// - wedge6: triangle 1-2-3, then 4-5-6 on the side its right-hand normal points to;
|
||||
/// - hex8/hex20: face 1-2-3-4, then 5-6-7-8 above it; hex20's mid-side nodes 9–12 lie on the
|
||||
/// edges of 1-2-3-4, 13–16 on those of 5-6-7-8, and 17–20 on edges 1-5, 2-6, 3-7, 4-8.
|
||||
///
|
||||
/// This is the Abaqus (and VTK) order. Formats that order nodes differently, such as Nastran's
|
||||
/// CHEXA with 20 nodes (vertical edges before the top face's edges), are reordered by their deck
|
||||
/// readers and writers; the model never stores another order.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ElementKind {
|
||||
/// Two-node axial member.
|
||||
@@ -148,6 +156,15 @@ pub struct Model {
|
||||
elements: BTreeMap<u32, Element>,
|
||||
}
|
||||
|
||||
/// Check that every coordinate is a finite number within ±[`MAX_COORD`] (the rule for nodes,
|
||||
/// shared by everything that takes coordinates from outside).
|
||||
pub fn check_coordinates(xyz: &[f64]) -> Result<()> {
|
||||
match xyz.iter().find(|c| !(c.abs() <= MAX_COORD)) {
|
||||
Some(c) => Err(Error::Invalid(format!("coordinate {c} is not a finite number within ±{MAX_COORD}"))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_label(what: &str, label: u32) -> Result<()> {
|
||||
if label == 0 || label > MAX_LABEL {
|
||||
return Err(Error::Invalid(format!("{what} label {label} is outside 1..={MAX_LABEL}")));
|
||||
@@ -175,9 +192,10 @@ impl Model {
|
||||
/// Add a node, or move an existing one (decks redefine nodes the same way).
|
||||
pub fn set_node(&mut self, label: u32, xyz: [f64; 3]) -> Result<()> {
|
||||
check_label("node", label)?;
|
||||
if let Some(c) = xyz.iter().find(|c| !(c.abs() <= MAX_COORD)) {
|
||||
return Err(Error::Invalid(format!("node {label}: coordinate {c} is not a finite number within ±{MAX_COORD}")));
|
||||
}
|
||||
check_coordinates(&xyz).map_err(|e| match e {
|
||||
Error::Invalid(m) => Error::Invalid(format!("node {label}: {m}")),
|
||||
e => e,
|
||||
})?;
|
||||
if !self.nodes.contains_key(&label) && self.nodes.len() >= MAX_NODES {
|
||||
return Err(Error::Limit(format!("a model holds at most {MAX_NODES} nodes")));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user