forked from 1b-agent/fieldcraft-upstream
mesh: the tet objective sees needles; no tet below scaled Jacobian 0.3 on the skill's tet parts
The improvement objective was the smallest dihedral sine, which misses needles: every dihedral angle fine while a corner is nearly flat, so the corpus meshes had scaled Jacobians down to 0.01-0.07. The objective is now the smaller of the dihedral sine and the scaled Jacobian at the worst corner (the measure analysts check, Knupp 2001), weighted so a Jacobian of 0.3 (the skill's limit) counts as a 30 degree dihedral angle (the schedule's target): the Jacobian decides only near that limit. The existing edge removal, multi-face removal, smoothing and insertion then remove the needles. Skill tet10 min SJ: sphere-on-plate 0.092 -> 0.335, filleted-bracket 0.021 -> 0.318 (both pass, nothing marked). Default-size tet10: thin-ribs 0.240, shallow-groove 0.321, rounded-box 0.311. The strong-relaxation test relaxes below 0.45: nothing is left below 0.3. Literature note in docs/meshing/literature.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
22ef56f323
commit
4329630943
8 files changed
+64
-17
No files matched your search
@@ -275,7 +275,8 @@ fn on_box_edge(p: [f64; 3]) -> bool {
|
||||
fn strong_relaxation_of_a_filleted_box_keeps_faces_and_edges() {
|
||||
let (_d, mut s, _) = filleted_box();
|
||||
let before = s.model.clone();
|
||||
let r = s.execute("mesh.node.relax", &json!({"mode": "strong", "bodies": ["B"], "below": 0.3})).unwrap();
|
||||
// The tet mesher leaves no element below the skill's 0.3, so relax those below 0.45.
|
||||
let r = s.execute("mesh.node.relax", &json!({"mode": "strong", "bodies": ["B"], "below": 0.45})).unwrap();
|
||||
let (b, a) = (&r["before"], &r["after"]);
|
||||
// The worst tets may not improve: their corners can all be held on the box's edges.
|
||||
assert!(a["min_scaled_jacobian"].as_f64().unwrap() >= b["min_scaled_jacobian"].as_f64().unwrap(), "{r}");
|
||||
|
||||
@@ -74,11 +74,14 @@ pub fn dihedral_cosines(x: &[P; 4]) -> [f64; 6] {
|
||||
})
|
||||
}
|
||||
|
||||
/// The quality objective: the smallest signed dihedral sine (1 is impossible; the regular
|
||||
/// tetrahedron scores √(8/9) ≈ 0.943). Positive for a valid tetrahedron, 0 for a flat one,
|
||||
/// negative for an inverted one (then the sine of largest magnitude, so worse inversions score
|
||||
/// lower). With `sin θ_ij = 6V ℓ_ij / (2A_k · 2A_l)`, the extreme sine is found on squared
|
||||
/// lengths and areas, with one square root.
|
||||
/// The quality objective of a valid tetrahedron: the smaller of its smallest dihedral sine and
|
||||
/// its weighted scaled Jacobian ([`JACOBIAN_WEIGHT`]; the regular tetrahedron scores
|
||||
/// √(8/9) ≈ 0.943). The dihedral sine catches slivers and caps; it does not see needles, whose
|
||||
/// dihedral angles can all be fine while a corner is nearly flat (Shewchuk 2002), and the scaled
|
||||
/// Jacobian `√2 · 6V / (ℓ₁ℓ₂ℓ₃)` at the worst corner, the measure analysts check (the skill's
|
||||
/// limit, [`crate::quality`]), does. 0 for a flat tetrahedron; for an inverted one the negative
|
||||
/// sine of largest magnitude, so worse inversions score lower. Extremes are found on squared
|
||||
/// lengths and areas, with one square root each.
|
||||
pub fn quality(x: &[P; 4]) -> f64 {
|
||||
let v6 = volume6(x);
|
||||
if v6 == 0.0 || !v6.is_finite() {
|
||||
@@ -95,9 +98,36 @@ pub fn quality(x: &[P; 4]) -> f64 {
|
||||
pick = if v6 > 0.0 { pick.min(r) } else { pick.max(r) };
|
||||
}
|
||||
let q = v6 * pick.sqrt();
|
||||
let q = if v6 > 0.0 { q.min(JACOBIAN_WEIGHT * scaled_jacobian(x, v6)) } else { q };
|
||||
if q.is_finite() { q } else { 0.0 }
|
||||
}
|
||||
|
||||
/// The weight of the scaled Jacobian in [`quality`]: the meshing skill's limit, a scaled
|
||||
/// Jacobian of 0.3, counts as much as a 30° dihedral angle (sin 30°, the improvement's target,
|
||||
/// [`super::improve::ImproveOptions::target`]), so the Jacobian decides only for tetrahedra near
|
||||
/// that limit (needles) and the dihedral sine everywhere else.
|
||||
pub const JACOBIAN_WEIGHT: f64 = 0.5 / 0.3;
|
||||
|
||||
/// The corners of a tetrahedron and the other three corners of each.
|
||||
const CORNERS: [(usize, [usize; 3]); 4] = [(0, [1, 2, 3]), (1, [0, 2, 3]), (2, [0, 1, 3]), (3, [0, 1, 2])];
|
||||
|
||||
/// The squared product of the three edge lengths at each corner.
|
||||
fn corner_products(x: &[P; 4]) -> [f64; 4] {
|
||||
CORNERS.map(|(c, o)| {
|
||||
o.iter().fold(1.0, |m, &j| {
|
||||
let e = sub(x[j], x[c]);
|
||||
m * dot(e, e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// The scaled Jacobian at the worst corner of a tetrahedron with six times the volume `v6`
|
||||
/// (as [`crate::quality`] reports it).
|
||||
fn scaled_jacobian(x: &[P; 4], v6: f64) -> f64 {
|
||||
let worst = corner_products(x).iter().fold(0.0_f64, |m, &p| m.max(p));
|
||||
std::f64::consts::SQRT_2 * v6 / worst.sqrt()
|
||||
}
|
||||
|
||||
/// The smallest and largest dihedral angle in degrees (reports only).
|
||||
pub fn dihedral_range(x: &[P; 4]) -> (f64, f64) {
|
||||
let (s, c) = (dihedral_sines(x), dihedral_cosines(x));
|
||||
@@ -221,6 +251,16 @@ mod tests {
|
||||
assert!(circumcenter(&flat).is_none());
|
||||
}
|
||||
|
||||
/// A needle: every dihedral angle above 20°, yet one corner nearly flat; the objective sees
|
||||
/// it through the scaled Jacobian.
|
||||
#[test]
|
||||
fn a_needle_scores_low() {
|
||||
let x = positive([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 0.866, 0.0], [0.5, 0.29, 12.0]]);
|
||||
let (lo, hi) = dihedral_range(&x);
|
||||
assert!(lo > 20.0 && hi < 120.0, "{lo} {hi}");
|
||||
assert!(quality(&x) < 0.15 * JACOBIAN_WEIGHT, "{}", quality(&x));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sliver_scores_low() {
|
||||
let x = positive([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.01]]);
|
||||
|
||||
@@ -433,7 +433,17 @@ mod determinism {
|
||||
let (p, t, f) = washer();
|
||||
let m = mesh(&p, &t, &f);
|
||||
assert!(m.elements.len() > 100, "{:?}", m.report);
|
||||
assert_eq!(fingerprint(&m), 6_285_915_972_108_509_003, "{} tets, {} nodes", m.elements.len(), m.points.len());
|
||||
// No needles either: the scaled Jacobian analysts check is above the skill's limit.
|
||||
let sj = m
|
||||
.elements
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let xyz: Vec<[f64; 3]> = e.iter().map(|&v| m.points[v as usize]).collect();
|
||||
crate::quality::element_quality(m.kind, &xyz).unwrap().get(crate::quality::Metric::ScaledJacobian).unwrap()
|
||||
})
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
assert!(sj >= 0.3, "min scaled Jacobian {sj}");
|
||||
assert_eq!(fingerprint(&m), 11_252_377_805_886_396_373, "{} tets, {} nodes", m.elements.len(), m.points.len());
|
||||
}
|
||||
|
||||
/// Listing the triangles, their corners and the vertices in another order, and numbering
|
||||
|
||||
@@ -52,10 +52,6 @@ const PLAIN_CUP: &str = r#"[
|
||||
/// Why a part fails the skill's limits today, read from its numbers and its section picture.
|
||||
/// Shown only while the part fails.
|
||||
const CAUSES: &[(&str, &str)] = &[
|
||||
(
|
||||
"filleted-bracket",
|
||||
"no structured method takes the whole body (the plan has no cuts for the fillet beside the holes), and tet10 at the plan's size leaves slivers (min SJ 0.04, 1709 marked). The same part partitioned into mapped cells (`filleted-bracket-blocks`) passes: partition (§3.4).",
|
||||
),
|
||||
(
|
||||
"loft-transition",
|
||||
"the lofted sides are swept one to one, and the elements where the section changes shape are skewed (min SJ 0.19). Relaxing the four marked elements did not raise it: the blocks are wrong, not the nodes (§6.3). A loft has no template yet (`unmapped`).",
|
||||
@@ -64,7 +60,6 @@ const CAUSES: &[(&str, &str)] = &[
|
||||
"sharp-wedge",
|
||||
"the wedge's sharp edge is swept as hex, so every element tapers to it (min SJ 0.01, edge ratio 150). A column of wedge6 at the edge (§4) raises the SJ but not the ratio: any element with a corner on a 2.9° edge has sides at least 1/tan 2.9° ≈ 20 times its width there, twice the limit. The edge has to be blunted (defeatured) or the limit waived along it.",
|
||||
),
|
||||
("sphere-on-plate", "no structured method takes the plate with the sunk sphere, and tet10 leaves slivers (min SJ 0.12, 662 marked)."),
|
||||
(
|
||||
"analytic-sphere",
|
||||
"no structured method takes a sphere (it has no face to sweep from or axis to revolve about), so the plan does not apply and the fallback's sphere template meshes it at the plan's size (2.45 mm, which keeps a sphere's chords to 0.75 % on paper), 1.1 % short of the volume: the template's elements at the cube's eight corners span more of the sphere than the size. A sphere template in the plan (§3) would size its caps; hex20 (`order: 2`) closes it too.",
|
||||
|
||||
@@ -402,7 +402,10 @@ Delaunay methods with Voronoi/ODT improvement, then (2) all-hex and hex-dominant
|
||||
(barycentres of boundary faces, of the tetrahedron, midpoints of boundary edges, and a point inside off a flat
|
||||
boundary edge). Deviations: the inserted vertex takes the Delaunay cavity rather than the optimal cut of §3.4; each pass
|
||||
smooths and flips the tetrahedra below sin 40° and inserts only when the worst did not improve; passes and the
|
||||
insertion list are capped. Objective: the minimum dihedral sine. Every operation is a checked replacement of a region
|
||||
insertion list are capped. Objective: the smaller of the minimum dihedral sine and the scaled Jacobian at the worst
|
||||
corner weighted so that a Jacobian of 0.3 counts as a 30° dihedral angle: the sine misses needles (every dihedral
|
||||
angle fine, one corner nearly flat; Shewchuk 2002); the scaled Jacobian analysts check (Knupp 2001, B6.6) catches
|
||||
them, and with the weight it decides only near that limit. Every operation is a checked replacement of a region
|
||||
(positive by exact `orient3d`, faces matched with opposite orientation, removed vertices fully inside), so the mesh is
|
||||
valid after each step.
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Times are wall clock on the machine that ran it.
|
||||
| bolted-assembly | plan (sweep), mesh.hex.generate (auto) | – | 6272 (6272 hex8) | 0.583 | 0.939 | 3.6 | 2.0e-4 | 0 | 0 (not relaxed) | 8 coincident, 0 cracks | – | 3.7 | pass | [x=50.000](skill/bolted-assembly.png) |
|
||||
| cantilever-i-beam | plan (sweep) | – | 61600 (61600 hex8) | 1.000 | 1.000 | 1.0 | 0.0e0 | 0 | 0 (not relaxed) | none | – | 8.5 | pass | [x=100.000](skill/cantilever-i-beam.png) |
|
||||
| cross-cylinders | none | – | – | – | – | – | – | – | – | – | – | 27.2 | no mesh | – |
|
||||
| filleted-bracket | tet10 | – | 21525 (21525 tet10) | 0.021 | 0.535 | 13.5 | -7.0e-6 | 0 | 1492 (not relaxed) | none | – | 15.9 | fail (SJ, ratio) | [x=30.000](skill/filleted-bracket.png) |
|
||||
| filleted-bracket | tet10 | – | 19991 (19991 tet10) | 0.318 | 0.564 | 4.1 | -6.8e-6 | 0 | 0 (not relaxed) | none | – | 15.1 | pass (fallback) | [x=30.000](skill/filleted-bracket.png) |
|
||||
| filleted-bracket-blocks | plan (sweep) | – | 75410 (75410 hex8) | 0.704 | 0.954 | 3.5 | 3.7e-5 | 0 | 0 (not relaxed) | none (after merging 2348 nodes) | – | 6.9 | pass | [x=30.000](skill/filleted-bracket-blocks.png) |
|
||||
| i-beam | plan (sweep) | – | 61600 (61600 hex8) | 1.000 | 1.000 | 1.0 | 0.0e0 | 0 | 0 (not relaxed) | none | – | 4.5 | pass | [z=100.000](skill/i-beam.png) |
|
||||
| loft-transition | plan (sweep) | – | 2873 (2873 hex8) | 0.189 | 0.959 | 1.7 | -2.1e-3 | 0 | 4 → 4 (4 relaxed, 0 undone; min SJ 0.189 → 0.189, max ratio 1.7 → 1.7) | none | – | 0.9 | fail (SJ) | [z=25.000](skill/loft-transition.png) |
|
||||
@@ -40,7 +40,7 @@ Times are wall clock on the machine that ran it.
|
||||
| rounded-box | plan (project) | 2 plane(s) | 11520 (11520 hex8) | 0.717 | 0.973 | 4.0 | -4.1e-3 | 0 | 0 (not relaxed) | none | – | 8.7 | pass | [x=20.000](skill/rounded-box.png) |
|
||||
| shallow-groove | plan (sweep) | – | 1960 (1960 hex8) | 0.991 | 0.999 | 1.1 | 2.7e-4 | 0 | 0 (not relaxed) | none | – | 0.2 | pass | [x=30.000](skill/shallow-groove.png) |
|
||||
| sharp-wedge | plan (sweep) | – | 1674 (1674 hex8) | 0.014 | 0.301 | 149.6 | 2.7e-15 | 0 | 1524 (not relaxed) | none | – | 0.2 | fail (SJ, ratio) | [x=40.000](skill/sharp-wedge.png) |
|
||||
| sphere-on-plate | tet10 | – | 23799 (23799 tet10) | 0.092 | 0.587 | 5.9 | -8.7e-5 | 0 | 679 (not relaxed) | none | – | 13.7 | fail (SJ) | [y=0.000](skill/sphere-on-plate.png) |
|
||||
| sphere-on-plate | tet10 | – | 25613 (25613 tet10) | 0.335 | 0.607 | 4.4 | -8.8e-5 | 0 | 0 (not relaxed) | none | – | 7.7 | pass (fallback) | [y=0.000](skill/sphere-on-plate.png) |
|
||||
| spline-vase | plan (sweep) | – | 4536 (4536 hex8) | 0.665 | 0.945 | 9.9 | -6.6e-3 | 0 | 0 (not relaxed) | none | – | 2.3 | pass | [z=-35.000](skill/spline-vase.png) |
|
||||
| stepped-block | plan (cells) | – | 1416 (1416 hex8) | 1.000 | 1.000 | 1.3 | -4.8e-15 | 0 | 0 (not relaxed) | none | – | 1.6 | pass | [x=30.000](skill/stepped-block.png) |
|
||||
| thin-ribs | plan (sweep) | – | 301308 (301308 hex8) | 0.999 | 1.000 | 1.3 | -3.4e-12 | 0 | 0 (not relaxed) | none | – | 34.7 | pass | [x=50.000](skill/thin-ribs.png) |
|
||||
@@ -75,7 +75,6 @@ limits the cause read from its numbers and section picture.
|
||||
- Bracket: mesh.plan.apply: mesh plan `Plan-Bracket`: the cells ["Bracket"] (cuts []) do not mesh all-hex: body `Bracket`: not supported yet: no structured hex method fits this body (extrude: cylindrical face 8 is not parallel to the direction; sweep: from face 1: lin…
|
||||
- Bracket: mesh.hex.generate: body `Bracket`: not supported yet: no structured hex method fits this body (extrude: cylindrical face 8 is not parallel to the direction; sweep: from face 1: linking face 6 has 2 boundary loops (periodic or holed linking faces are not swept…
|
||||
- Bracket: mesh.hex.project (sphere): body `Bracket`: meshing failed: 91 of 5104 elements are inverted, twisted or degenerate after projection (certified Jacobian; smallest corner scaled Jacobian -0.760): block 1 cell [3, 3, 0] near (12.519, 11.405, 1.604); block 1 cell [3, 4, …
|
||||
- **fail (SJ, ratio)**: no structured method takes the whole body (the plan has no cuts for the fillet beside the holes), and tet10 at the plan's size leaves slivers (min SJ 0.04, 1709 marked). The same part partitioned into mapped cells (`filleted-bracket-blocks`) passes: partition (§3.4).
|
||||
- `loft-transition` (corpus: A 40 mm square lofted to an R15 circle 50 mm above: a ruled free-form surface between unlike sections.):
|
||||
- **fail (SJ)**: the lofted sides are swept one to one, and the elements where the section changes shape are skewed (min SJ 0.19). Relaxing the four marked elements did not raise it: the blocks are wrong, not the nodes (§6.3). A loft has no template yet (`unmapped`).
|
||||
- `sharp-wedge` (corpus: A wedge whose faces meet at under 3 degrees: a sharp edge with a needle-thin cross-section.):
|
||||
@@ -84,7 +83,6 @@ limits the cause read from its numbers and section picture.
|
||||
- Plate: mesh.plan.apply: mesh plan `Plan-Plate`: the cells ["Plate"] (cuts []) do not mesh all-hex: body `Plate`: not supported yet: no structured hex method fits this body (extrude: face 6 is curved across the direction; sweep: from face 0: 6 faces are not on the …
|
||||
- Plate: mesh.hex.generate: body `Plate`: not supported yet: no structured hex method fits this body (extrude: face 6 is curved across the direction; sweep: from face 0: 6 faces are not on the sweep path; from face 5: linking face 11 has 2 boundary loops (periodic or …
|
||||
- Plate: mesh.hex.project (sphere): body `Plate`: meshing failed: the mesh volume 32917.42909007507 is far from the body's 69353.30815403847: the blocks do not cover the part (every outer block face must stand for part of its surface)
|
||||
- **fail (SJ)**: no structured method takes the plate with the sunk sphere, and tet10 leaves slivers (min SJ 0.12, 662 marked).
|
||||
- `analytic-sphere` (corpus: Sphere r 20: curvature everywhere, no edges.):
|
||||
- Sphere: mesh.plan.apply: mesh plan `Plan-Sphere`: the cells ["Sphere"] (cuts []) do not mesh all-hex: body `Sphere`: not supported yet: no structured hex method fits this body (extrude: no direction to try; sweep: no planar face to sweep from; revolve: no axis to t…
|
||||
- Sphere: mesh.hex.generate: body `Sphere`: not supported yet: no structured hex method fits this body (extrude: no direction to try; sweep: no planar face to sweep from; revolve: no axis to try; mapped: meshing failed: meshing failed: mesh validity gate: 8 inverted or…
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 107 KiB |
Reference in new issue
Block a user