Files
OneCAD-Simulation/crates/engine/src/cmd/relax/tests.rs
T

195 lines
8.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::*;
/// The plate (40 × 30 × 10) with its Ø10 hole round (15, 15): a point on the hole's wall.
const ON_HOLE: [f64; 3] = [20.0, 15.0, 5.0];
const HOLE: ([f64; 2], f64) = ([15.0, 15.0], 5.0);
fn plate() -> Session {
super::super::datum::tests::plate().0
}
fn hole_radius(p: [f64; 3]) -> f64 {
((p[0] - HOLE.0[0]).powi(2) + (p[1] - HOLE.0[1]).powi(2)).sqrt()
}
/// Which analytic face a node of the plate lies on, if any.
fn on_faces(p: [f64; 3]) -> Vec<&'static str> {
let mut f = Vec::new();
if (hole_radius(p) - HOLE.1).abs() < 1e-6 {
f.push("hole");
}
if p[2].abs() < 1e-6 || (p[2] - 10.0).abs() < 1e-6 {
f.push("cap");
}
f
}
/// Spikes near the hole: interior nodes pushed about; nodes on the hole turned round it and
/// moved along its axis (still on the cylinder), nodes on the caps moved in their plane.
fn perturb(s: &mut Session, centre: [f64; 3], radius: f64, amount: f64) {
let nodes: Vec<(u32, [f64; 3])> = s.model.nodes().iter().map(|(n, p)| (*n, p.xyz)).collect();
for (k, (n, p)) in nodes.into_iter().enumerate() {
let d2: f64 = (0..3).map(|i| (p[i] - centre[i]).powi(2)).sum();
if d2 > radius * radius {
continue;
}
let wiggle = |m: usize| amount * ((k * m % 7) as f64 - 3.0) / 3.0;
let faces = on_faces(p);
let q = match faces.as_slice() {
[] => [p[0] + wiggle(3), p[1] + wiggle(5), p[2] + wiggle(11)],
["hole"] => {
let a = wiggle(3) / HOLE.1;
let (x, y) = (p[0] - HOLE.0[0], p[1] - HOLE.0[1]);
[HOLE.0[0] + x * a.cos() - y * a.sin(), HOLE.0[1] + x * a.sin() + y * a.cos(), p[2] + wiggle(5)]
}
["cap"] if hole_radius(p) > HOLE.1 + 1e-6 => [p[0] + wiggle(3), p[1] + wiggle(5), p[2]],
_ => continue,
};
s.model.set_node(n, q).unwrap();
}
}
fn check_on_geometry(before: &Model, after: &Model) {
for (n, p) in after.nodes() {
let old = before.node(*n).unwrap().xyz;
for f in on_faces(old) {
match f {
"hole" => assert!((hole_radius(p.xyz) - HOLE.1).abs() < 1e-6, "node {n} left the hole: r = {}", hole_radius(p.xyz)),
_ => assert!((p.xyz[2] - old[2]).abs() < 1e-6, "node {n} left its cap"),
}
}
}
}
fn invalid(s: &mut Session) -> u64 {
s.execute("mesh.quality", &json!({})).unwrap()["invalid"]["count"].as_u64().unwrap_or(0)
}
/// The issue's hex test on a design: spikes near the hole relax, every surface node stays on its
/// face (the hole's cylinder exactly), quality rises, nothing inverts, nothing outside the sphere
/// moves; one undo step; the field is kept and saved with the model.
#[test]
fn hex_spikes_near_a_hole_relax_on_the_geometry() {
let mut s = plate();
s.execute("mesh.hex.generate", &json!({"size": 2.0})).unwrap();
perturb(&mut s, ON_HOLE, 3.0, 0.35);
let perturbed = s.model.clone();
let r = s.execute("mesh.node.relax", &json!({"center": ON_HOLE, "radius": 4.0, "iterations": 10})).unwrap();
let (before, after) = (r["before"]["min_scaled_jacobian"].as_f64().unwrap(), r["after"]["min_scaled_jacobian"].as_f64().unwrap());
assert!(after > before + 0.05, "{r}");
assert_eq!(r["after"]["invalid"], 0, "{r}");
assert!(r["moved"].as_u64().unwrap() > 0 && r["field"] == "Relax1", "{r}");
check_on_geometry(&perturbed, &s.model);
for (n, p) in s.model.nodes() {
let old = perturbed.node(*n).unwrap().xyz;
if old != p.xyz {
let d2: f64 = (0..3).map(|i| (old[i] - ON_HOLE[i]).powi(2)).sum();
assert!(d2 < 16.0, "node {n} outside the sphere moved");
}
}
assert_eq!(invalid(&mut s), 0);
// The field is saved with the model.
let text = serde_json::to_string(&s.model).unwrap();
let back: Model = serde_json::from_str(&text).unwrap();
assert_eq!(back.relaxation("Relax1"), s.model.relaxation("Relax1"));
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.model, perturbed);
}
/// The same on tet10 with curved mid-side nodes on the hole: they stay on the cylinder.
#[test]
fn tet10_relaxes_with_curved_mid_side_nodes_on_the_hole() {
let mut s = plate();
s.execute("mesh.tet.generate", &json!({"size": 2.5, "order": "quadratic"})).unwrap();
perturb(&mut s, ON_HOLE, 3.0, 0.2);
// Put the mid-side nodes back on their edges' middles, on the hole where the edge is.
let mids: Vec<(u32, u32, u32)> = s
.model
.elements()
.values()
.flat_map(|e| e.kind.edges().iter().map(|ed| (e.nodes[ed[0]], e.nodes[ed[1]], e.nodes[ed[2]])).collect::<Vec<_>>())
.collect();
let original = s.model.clone();
for (a, b, m) in mids {
let (pa, pb, pm) = (original.node(a).unwrap().xyz, original.node(b).unwrap().xyz, original.node(m).unwrap().xyz);
let mut q = [(pa[0] + pb[0]) / 2.0, (pa[1] + pb[1]) / 2.0, (pa[2] + pb[2]) / 2.0];
if on_faces(pm).contains(&"hole") {
let r = hole_radius(q);
q = [HOLE.0[0] + (q[0] - HOLE.0[0]) * HOLE.1 / r, HOLE.0[1] + (q[1] - HOLE.0[1]) * HOLE.1 / r, q[2]];
}
s.model.set_node(m, q).unwrap();
}
let perturbed = s.model.clone();
let r = s.execute("mesh.node.relax", &json!({"center": ON_HOLE, "radius": 4.0, "iterations": 8})).unwrap();
assert!(r["after"]["min_scaled_jacobian"].as_f64().unwrap() >= r["before"]["min_scaled_jacobian"].as_f64().unwrap(), "{r}");
assert_eq!(r["after"]["invalid"], 0, "{r}");
check_on_geometry(&perturbed, &s.model);
}
/// A partition cut is not geometry: nodes on it move freely, shared by the cells on both sides.
#[test]
fn nodes_on_a_partition_cut_move_freely() {
let mut s = plate();
s.execute("geometry.datum", &json!({"name": "Cut", "by": {"type": "offset", "plane": "yz", "offset": 27}})).unwrap();
s.execute("geometry.partition", &json!({"name": "P", "datum": "Cut"})).unwrap();
s.execute("mesh.cells", &json!({"size": 2.0, "default": "hex"})).unwrap();
let centre = [27.0, 15.0, 5.0];
perturb(&mut s, centre, 2.5, 0.3);
let before = s.model.clone();
s.execute("mesh.node.relax", &json!({"center": centre, "radius": 3.5, "iterations": 10})).unwrap();
let moved_off = s
.model
.nodes()
.iter()
.filter(|(n, p)| {
let old = before.node(**n).unwrap().xyz;
(old[0] - 27.0).abs() < 1e-6 && on_faces(old).is_empty() && (p.xyz[0] - 27.0).abs() > 1e-6
})
.count();
assert!(moved_off > 0, "nodes on the cut are free");
assert_eq!(invalid(&mut s), 0);
}
/// A field marked `reapply` is applied again after a remesh; `mesh.node.relax.apply` applies it
/// on demand; around the worst element works.
#[test]
fn a_kept_field_is_applied_again_after_a_remesh() {
let mut s = plate();
s.execute("mesh.hex.generate", &json!({"size": 3.0})).unwrap();
perturb(&mut s, ON_HOLE, 4.0, 0.3);
let r = s.execute("mesh.node.relax", &json!({"center": ON_HOLE, "radius": 5.0, "field": "Spike", "reapply": true})).unwrap();
assert_eq!(r["field"], "Spike");
let r = s.execute("mesh.hex.generate", &json!({"size": 1.5})).unwrap();
assert_eq!(r["relaxations_applied"][0]["field"], "Spike", "{r}");
assert_eq!(invalid(&mut s), 0);
let r = s.execute("mesh.node.relax.apply", &json!({"field": "Spike"})).unwrap();
assert!(r["summary"].as_str().unwrap().contains("Spike"));
let worst = s.execute("mesh.quality", &json!({})).unwrap()["metrics"]["scaled_jacobian"]["worst"].as_u64().unwrap();
let r = s.execute("mesh.node.relax", &json!({"around_element": worst})).unwrap();
assert!(r["after"]["min_scaled_jacobian"].as_f64() >= r["before"]["min_scaled_jacobian"].as_f64(), "{r}");
}
#[test]
fn hostile_parameters_never_panic() {
let mut s = plate();
s.execute("mesh.hex.generate", &json!({"size": 4.0})).unwrap();
for p in [
json!({}),
json!({"center": [0, 0, 0]}),
json!({"center": [1e300, 0, 0], "radius": 1}),
json!({"center": [0, 0, 0], "radius": -1}),
json!({"center": [0, 0, 0], "radius": 1, "nodes": [1]}),
json!({"nodes": [1], "strength": 2}),
json!({"nodes": [1], "iterations": 0}),
json!({"nodes": [1], "iterations": 1000}),
json!({"nodes": [1], "falloff": "cubic"}),
json!({"around_element": 99999999}),
json!({"set": "nope"}),
json!({"nodes": [1], "bodies": ["nope"]}),
] {
assert!(s.execute("mesh.node.relax", &p).is_err(), "{p}");
}
assert!(s.execute("mesh.node.relax.apply", &json!({"field": "nope"})).is_err());
assert!(s.execute("mesh.node.relax.apply", &json!({})).is_err());
}