forked from 1b-agent/fieldcraft-upstream
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:
1 parent
fed27d816d
commit
9469ac0853
53 files changed
+5880
-278
No files matched your search
@@ -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)))
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user