forked from 1b-agent/fieldcraft-upstream
mesh: smoothing lifts elements up to 0.7 and visits mirror twins alike (#402)
The compass search after the sweeps only lifted elements below 0.5 (the quality floor), so a mesh whose worst elements sat just above it stayed there: every corner of an enclosure base stopped at a scaled Jacobian of 0.50-0.52 where its walls meet the corner bosses. It now lifts every element below 0.7, never lowering another below the lower of its own value and 0.5. The sweeps and the search are Gauss-Seidel, so the node order shaped the result and mirror image corners smoothed differently. Nodes are now visited by a key that a mirror or a quarter turn about the nodes' centroid keeps (distance from it, then the sorted offsets along the axes). What spread is left between mirrored corners (0.686 vs 0.702 on the test base) is most likely the projection onto the body's tessellation, which the kernel does not make mirror symmetric (not yet proven). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
e79ba866a1
commit
4d6d69ee6d
3 files changed
+86
-4
No files matched your search
@@ -1559,6 +1559,47 @@ mod acceptance {
|
||||
accept(&mut s, &r, 0.6, 6.0);
|
||||
}
|
||||
|
||||
/// The enclosure base of the test below without its standoffs: symmetric across x = 28 and
|
||||
/// y = 21, so its four corners mesh alike: the smallest scaled Jacobian round each corner
|
||||
/// boss well above 0.5 (the compass search lifts elements up to 0.7) and nearly the same.
|
||||
#[test]
|
||||
fn an_enclosure_base_meshes_its_four_corners_alike() {
|
||||
let mut commands = vec![
|
||||
json!({"command": "solid.box", "params": {"length": 56, "width": 42, "height": 16, "body_name": "Base"}}),
|
||||
json!({"command": "solid.fillet", "params": {"edges": [[0, 0, 8], [56, 0, 8], [56, 42, 8], [0, 42, 8]], "radius": 5}}),
|
||||
json!({"command": "solid.shell", "params": {"faces": [[28, 21, 16]], "thickness": 3, "body": "Base"}}),
|
||||
];
|
||||
let corners = [(-1.0, -1.0), (57.0, -1.0), (-1.0, 43.0), (57.0, 43.0)];
|
||||
for (x, y) in corners {
|
||||
commands.push(json!({"command": "solid.cylinder", "params": {"diameter": 13, "height": 16, "base": [x, y, 0], "operation": "join", "targets": ["Base"]}}));
|
||||
}
|
||||
for (x, y) in corners {
|
||||
commands.push(json!({"command": "solid.hole", "params": {"position": [x, y, 16], "diameter": 3.3, "depth": 8}}));
|
||||
}
|
||||
let (_d, mut s) = open(json!(commands));
|
||||
let plan = s.execute("mesh.plan", &json!({"body": "Base", "size": 1.5, "layers": 2})).unwrap();
|
||||
let r = s.execute("mesh.plan.apply", &json!({"plan": plan["name"]})).unwrap();
|
||||
accept(&mut s, &r, 0.5, 10.0);
|
||||
// Every element's scaled Jacobian, by the corner (quadrant) its centroid lies in.
|
||||
let m = s.execute("mesh.quality.mark", &json!({"limits": {"scaled_jacobian": 1.01, "min_dihedral": null, "max_dihedral": null, "aspect_ratio": null, "min_angle": null, "max_angle": null}})).unwrap();
|
||||
let mut worst = [f64::INFINITY; 4];
|
||||
for e in m["elements"].as_array().unwrap() {
|
||||
let at = &e["at"];
|
||||
let q = usize::from(at[0].as_f64().unwrap() > 28.0) + 2 * usize::from(at[1].as_f64().unwrap() > 21.0);
|
||||
for f in e["failed"].as_array().unwrap() {
|
||||
if f["metric"] == "scaled_jacobian" {
|
||||
worst[q] = worst[q].min(f["value"].as_f64().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
// The layout is mirror symmetric and the smoother visits mirror twins alike; what is left
|
||||
// is the projection onto the body's tessellation, which the kernel does not make mirror
|
||||
// symmetric (a chordal tolerance of 0.0165 mm, on 1.5 mm elements).
|
||||
let (lo, hi) = worst.iter().fold((f64::INFINITY, 0.0f64), |(l, h), w| (l.min(*w), h.max(*w)));
|
||||
assert!(hi - lo < 0.03, "per corner min scaled Jacobian {worst:?}: {r}");
|
||||
assert!(lo > 0.65, "per corner min scaled Jacobian {worst:?}");
|
||||
}
|
||||
|
||||
/// An enclosure base: a box with rounded vertical edges, shelled open at the top, a screw
|
||||
/// boss joined at each corner (centred outside it, nearly touching the inner fillet) with a
|
||||
/// blind hole, and standoffs on the floor. Each boss's ring frames the whole circle,
|
||||
|
||||
@@ -197,9 +197,13 @@ fn squeezed_junction_elements_are_reported_not_meshed() {
|
||||
{"op": "connect", "from": [45, "+z"], "to": [47, "-z"], "layers": 2},
|
||||
{"op": "remove", "blocks": [1, 2, 3, 4, 5]}
|
||||
]);
|
||||
// Smoothing lifts its worst elements just over the default floor (0.21): the squeeze is
|
||||
// reported at any floor an analyst would accept.
|
||||
p["min_quality"] = json!(0.3);
|
||||
let model = s.model.clone();
|
||||
let e = s.execute("mesh.hex.project", &p).unwrap_err().to_string();
|
||||
assert!(e.contains("smallest scaled Jacobian") && e.contains("worst in block 34"), "{e}");
|
||||
// Smoothing leaves the worst element in the squeezed ring block (34) or in block 36.
|
||||
assert!(e.contains("smallest scaled Jacobian") && (e.contains("worst in block 34") || e.contains("worst in block 36")), "{e}");
|
||||
assert_eq!(s.model, model);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user