Tet mesh quality: one validity criterion, Delaunay refinement, ODT, Klingner–Shewchuk, tet10

The end-to-end demo met a raw-CDT sliver that the validity gate accepted (exactly positive) and
the solver refused. Now:

- One validity criterion, `fieldcraft_model::admissible_jacobian` (normalized Jacobian above
  1e-12), used by the implicit solver (fem `positive_inverse`), the explicit tet4/hex8
  kinematics and the mesh gate, which proves it on the whole element (tet4: the solvers' own
  Jacobian; curved kinds: a Bernstein floor), so whatever passes the gate passes the solvers.
- CDT rounding repaired by flips on the rounded mesh (Diazzi et al. 2023 §5.1) instead of the
  ±2-ulp nudge.
- `fieldcraft-mesh::tet`: a mutable tetrahedral complex whose every change is a checked
  replacement (positive and admissible tetrahedra, faces matched with opposite orientation) with
  an exact undo log; edge removal (Klincsek), multi-face removal, Delaunay insertion with
  boundary faces, edge collapse; a curvature- and thickness-graded size field; constrained
  Delaunay refinement with encroachment splits; ODT smoothing; the Klingner–Shewchuk schedule;
  boundary vertices moved onto the exact B-rep surfaces at the end; tet10 with mid-edge nodes on
  the geometry.
- `mesh.tet.generate` gains size, order, quality, quality_target, curvature_angle, grading,
  max_passes, max_points and reports the CDT's and the mesh's quality; `solve.probe` takes a set
  and reports its largest von Mises stress.
- Oracle cases filleted-bracket-fillet-stress(-coarse): the fillet's peak von Mises stress on
  tet10 meshes of 2k and 13.5k elements, 213.3 MPa (a four-level study stays within 1.8%); the
  CDT cases keep the raw tetrahedralization.
- Corpus quality columns: all 20 valid parts mesh and pass the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 08:28:49 -04:00
1 parent fed27d816d
commit 9469ac0853
53 files changed
+5880 -278

No files matched your search

+6
View File
@@ -296,6 +296,12 @@ impl Bernstein {
Ok((a, b))
}
/// The coefficients of the used slots, in a fixed order shared by every polynomial on the
/// same domain (so the coefficients of a vector-valued polynomial's components line up).
pub fn coefficients(&self) -> Vec<f64> {
self.multis().iter().map(|m| self.get(m)).collect()
}
/// The smallest and largest coefficient: bounds of the polynomial on its domain.
pub fn bounds(&self) -> (f64, f64) {
self.multis().iter().map(|m| self.get(m)).fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), c| (lo.min(c), hi.max(c)))
+25 -5
View File
@@ -32,7 +32,7 @@ pub(crate) const HEX_CORNERS: [[usize; 3]; 8] = [[0, 0, 0], [1, 0, 0], [1, 1, 0]
/// Hex edges in the order of hex20's mid-side nodes.
pub(crate) const HEX_EDGES: [(usize, usize); 12] = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)];
/// Tet edges in the order of tet10's mid-side nodes.
pub(crate) const TET_EDGES: [(usize, usize); 6] = [(0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)];
pub const TET_EDGES: [(usize, usize); 6] = [(0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)];
/// A Jacobian determinant is positive when it exceeds this fraction of its largest magnitude.
/// Anything smaller is degenerate to rounding and counts as invalid.
@@ -280,15 +280,35 @@ impl Piece {
}
}
/// Decide the sign of a Jacobian determinant on the whole reference domain.
pub(crate) fn certify(det: Bernstein) -> Result<JacobianCheck> {
/// An upper bound of `|∂x/∂ξ_a|` over the whole element for one Jacobian column (a vector
/// polynomial in Bernstein form): the longest control vector, since the polynomial stays in the
/// convex hull of its control points.
fn column_norm_bound(column: &[Bernstein; 3]) -> f64 {
let [x, y, z] = column.each_ref().map(Bernstein::coefficients);
x.iter().zip(&y).zip(&z).map(|((a, b), c)| (a * a + b * b + c * c).sqrt()).fold(0.0, f64::max)
}
/// The determinant's floor for [`certify`] that proves FieldCraft's validity criterion
/// ([`fieldcraft_model::admissible_jacobian`]) on the whole element: with `N` the product of the
/// columns' norm bounds, `det J(ξ) > 2 · MIN_NORMALIZED_JACOBIAN · N` everywhere gives
/// `det J(ξ) / Π|J_a(ξ)| > MIN_NORMALIZED_JACOBIAN` at every point, so the solvers, which check
/// the same criterion at their integration points, accept every element the gate accepts. The
/// factor 2 covers the rounding of the solvers' own Jacobian evaluation.
pub(crate) fn admissible_floor(columns: &[[Bernstein; 3]; 3]) -> f64 {
let n: f64 = columns.iter().map(column_norm_bound).product();
2.0 * fieldcraft_model::MIN_NORMALIZED_JACOBIAN * n
}
/// Decide whether a Jacobian determinant exceeds `floor` (and [`RELATIVE_TOLERANCE`] of its
/// largest magnitude) on the whole reference domain.
pub(crate) fn certify(det: Bernstein, floor: f64) -> Result<JacobianCheck> {
let (lo, hi) = det.bounds();
let volume = det.integral();
let scale = lo.abs().max(hi.abs());
if !scale.is_finite() || !volume.is_finite() {
if !scale.is_finite() || !volume.is_finite() || !floor.is_finite() {
return Err(Error::Invalid("the element's Jacobian is not a finite number".into()));
}
let tol = RELATIVE_TOLERANCE * scale;
let tol = (RELATIVE_TOLERANCE * scale).max(floor);
let ratio_of = |v: f64| if scale > 0.0 { v / scale } else { 0.0 };
let verts = det.factors().iter().map(|f| (0..=f.dim).map(|k| simplex_vertex(k, 1).map(|x| x as f64)).collect()).collect();
let mut stack = vec![Piece::new(det, verts, 0)];
+17 -7
View File
@@ -14,16 +14,22 @@
//! Inputs are hostile: coordinates must be finite and within [`fieldcraft_model::MAX_COORD`],
//! and every metric has a defined value on degenerate and inverted elements.
//!
//! Linear tetrahedra take their validity from the exact `orient3d` ([`crate::predicates`]); the
//! other kinds from the floating-point Bernstein certificate, which counts a Jacobian within
//! [`jacobian::RELATIVE_TOLERANCE`] of zero as degenerate.
//! Validity is FieldCraft's one criterion, shared with the solvers: an element is admissible when
//! its normalized Jacobian `det J / Π|∂x/∂ξ_a|` exceeds
//! [`fieldcraft_model::MIN_NORMALIZED_JACOBIAN`] ([`fieldcraft_model::admissible_jacobian`]).
//! Linear tetrahedra take their orientation from the exact `orient3d` ([`crate::predicates`]) and
//! must also pass the criterion on the solvers' own Jacobian (the edges of the input coordinates),
//! so the gate and the solvers decide every tet4 alike. The other kinds use the floating-point
//! Bernstein certificate, which proves the criterion on the whole element (and counts a Jacobian
//! within [`jacobian::RELATIVE_TOLERANCE`] of zero as degenerate); the solvers check it only at
//! their integration points, so whatever passes the gate passes the solvers.
mod bernstein;
pub mod jacobian;
mod report;
pub(crate) mod shape;
use fieldcraft_model::{ElementKind, check_coordinates};
use fieldcraft_model::{ElementKind, admissible_jacobian, check_coordinates, tet4_jacobian};
pub use jacobian::{JacobianCheck, Validity};
pub use report::{
@@ -156,15 +162,19 @@ fn checked(kind: ElementKind, xyz: &[[f64; 3]], p: &[[f64; 3]]) -> Result<(Optio
{
let [a, b, c, d]: [[f64; 3]; 4] = corners(p)?;
let volume = shape::det(shape::sub(b, a), shape::sub(c, a), shape::sub(d, a)) / 6.0;
// FieldCraft's one criterion, on the solvers' own Jacobian (edges of the input
// coordinates); the exact sign settles the orientation of the rest.
let admissible = admissible_jacobian(&tet4_jacobian(&corners(xyz)?));
let (validity, ratio) = match sign {
Sign::Positive => (Validity::Valid, 1.0),
Sign::Positive if admissible => (Validity::Valid, 1.0),
Sign::Positive | Sign::Zero => (Validity::Invalid, 0.0),
Sign::Negative => (Validity::Invalid, -1.0),
Sign::Zero => (Validity::Invalid, 0.0),
};
return Ok((None, JacobianCheck { validity, ratio, volume }));
}
let jacobian::Jacobian { columns, det } = jacobian::jacobian(kind, p)?;
Ok((Some(columns), jacobian::certify(det)?))
let floor = jacobian::admissible_floor(&columns);
Ok((Some(columns), jacobian::certify(det, floor)?))
}
/// The exact orientation of a tetrahedron (positive for node 4 on the side of face 1-2-3's
+5 -2
View File
@@ -78,9 +78,12 @@ fn inverted_and_flat_tetrahedra() {
assert_eq!(metric(&q, Metric::AspectRatio), f64::INFINITY);
assert!(q.metrics.iter().all(|(_, v)| !v.is_nan()));
// A sliver with an exactly positive orientation is valid, however thin (exact orient3d).
// A sliver with an exactly positive orientation but a normalized Jacobian below the one
// criterion the solvers share is invalid; a thin one above it is valid.
let sliver = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0.3, 0.3, 1e-30]];
assert_eq!(element_validity(ElementKind::Tet4, &sliver).unwrap().validity, Validity::Valid);
assert_eq!(element_validity(ElementKind::Tet4, &sliver).unwrap().validity, Validity::Invalid);
let thin = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0.3, 0.3, 1e-9]];
assert_eq!(element_validity(ElementKind::Tet4, &thin).unwrap().validity, Validity::Valid);
let below = [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0.3, 0.3, -1e-30]];
assert_eq!(element_validity(ElementKind::Tet4, &below).unwrap().validity, Validity::Invalid);