forked from 1b-agent/fieldcraft-upstream
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 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
77 lines
3.6 KiB
Rust
77 lines
3.6 KiB
Rust
//! One validity criterion: the meshers' gate and both solvers decide every element alike.
|
|
//!
|
|
//! The end-to-end demo of 2026-10-13 met a raw CDT sliver that the gate accepted (its exact
|
|
//! orientation is positive) and the solver refused (its Jacobian was below the solver's
|
|
//! tolerance). Both now use `fieldcraft_model::admissible_jacobian`: the gate on the whole
|
|
//! element, the implicit solver at its integration points, the explicit tetrahedron on its one
|
|
//! Jacobian.
|
|
|
|
use fieldcraft_fem::uniform::UniformStrain;
|
|
use fieldcraft_fem::{Formulation, Solid, material::isotropic};
|
|
use fieldcraft_mesh::quality::{Validity, element_validity};
|
|
use fieldcraft_model::{ElementKind, MIN_NORMALIZED_JACOBIAN, normalized_jacobian, tet4_jacobian};
|
|
|
|
fn implicit_accepts(kind: ElementKind, xyz: &[[f64; 3]]) -> bool {
|
|
let d = isotropic(210_000.0, 0.3).unwrap();
|
|
Solid::new(kind, xyz, Formulation::Full).and_then(|e| e.stiffness(&d)).is_ok()
|
|
}
|
|
|
|
fn gate_accepts(kind: ElementKind, xyz: &[[f64; 3]]) -> bool {
|
|
element_validity(kind, xyz).map(|c| c.validity == Validity::Valid).unwrap_or(false)
|
|
}
|
|
|
|
/// Slivers flattening through the criterion's threshold, near and far from the origin: the gate,
|
|
/// the implicit solver and the explicit tetrahedron agree on every one, and some of them are
|
|
/// exactly positive yet inadmissible (the case the demo met).
|
|
#[test]
|
|
fn gate_and_solvers_agree_on_slivers() {
|
|
let mut refused_positive = 0;
|
|
let mut accepted = 0;
|
|
for offset in [0.0, 1e3, -7.25e4] {
|
|
for k in 0..400 {
|
|
// Heights from 1e-17 to 1e-7 of the edge length, on a log scale with jitter.
|
|
let h = 10f64.powf(-17.0 + 10.0 * k as f64 / 400.0) * (1.0 + 0.37 * ((k * 7919) % 13) as f64 / 13.0);
|
|
let o = [offset, 0.5 * offset, -offset];
|
|
let xyz = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.4, 0.35, h]].map(|p| [p[0] + o[0], p[1] + o[1], p[2] + o[2]]);
|
|
let gate = gate_accepts(ElementKind::Tet4, &xyz);
|
|
let implicit = implicit_accepts(ElementKind::Tet4, &xyz);
|
|
let explicit = UniformStrain::tet4(&xyz).is_ok();
|
|
assert_eq!(
|
|
(gate, implicit, explicit),
|
|
(gate, gate, gate),
|
|
"h = {h:e}, offset {offset}: gate {gate}, implicit {implicit}, explicit {explicit}"
|
|
);
|
|
let q = normalized_jacobian(&tet4_jacobian(&xyz));
|
|
if !gate && q > 0.0 {
|
|
refused_positive += 1;
|
|
}
|
|
accepted += usize::from(gate);
|
|
assert_eq!(gate, q > MIN_NORMALIZED_JACOBIAN, "h = {h:e}");
|
|
}
|
|
}
|
|
assert!(refused_positive > 0 && accepted > 0, "{refused_positive} refused, {accepted} accepted");
|
|
}
|
|
|
|
/// A curved tetrahedron the gate accepts is accepted by the implicit solver at every integration
|
|
/// point; the gate proves the criterion on the whole element.
|
|
#[test]
|
|
fn gate_accepts_only_what_the_solver_accepts_for_tet10() {
|
|
let corners = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.3, 0.3, 1.0]];
|
|
let edges = [(0, 1), (1, 2), (2, 0), (0, 3), (1, 3), (2, 3)];
|
|
for k in 0..200 {
|
|
let bend = -0.6 + 1.2 * k as f64 / 200.0;
|
|
let mut xyz: Vec<[f64; 3]> = corners.to_vec();
|
|
for (e, &(a, b)) in edges.iter().enumerate() {
|
|
let (p, q): ([f64; 3], [f64; 3]) = (corners[a], corners[b]);
|
|
let mut m = [0.5 * (p[0] + q[0]), 0.5 * (p[1] + q[1]), 0.5 * (p[2] + q[2])];
|
|
if e == 1 {
|
|
m[2] += bend;
|
|
}
|
|
xyz.push(m);
|
|
}
|
|
if gate_accepts(ElementKind::Tet10, &xyz) {
|
|
assert!(implicit_accepts(ElementKind::Tet10, &xyz), "bend {bend}");
|
|
}
|
|
}
|
|
}
|