Files
OneCAD-Simulation/crates/mesh/tests/predicates_bench.rs
T
jyvikoandClaude Opus 5.5 d5bd74570f Rename the project to FieldCraft
Crates fieldcraft-*, binaries fieldcraft and fieldcraft-cli, the .fieldcraft file (format tag
fieldcraft/1, manifest fieldcraft.json), FIELDCRAFT_* environment variables, the MCP server
name, egui widget ids (fc_*), the app icon document FieldCraft.icon and its generated family,
docs and licences. No compatibility shim: no files of the earlier name exist.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-13 23:51:40 -04:00

183 lines
7.4 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.
//! Throughput and filter hit rate of the exact predicates. Ignored by default; run with
//! `cargo test --release -p fieldcraft-mesh --test predicates_bench -- --ignored --nocapture`.
use std::hint::black_box;
use std::time::Instant;
use fieldcraft_mesh::predicates::{
GenericPoint, Lpi, Point3, Sign, generic_orient3d, insphere, insphere_filter, insphere_sos, orient3d, orient3d_filter,
};
struct Rng(u64);
impl Rng {
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn unit(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
fn index(&mut self, n: usize) -> usize {
(self.next_u64() % n as u64) as usize
}
}
fn k3(p: &Point3) -> robust::Coord3D<f64> {
let [x, y, z] = p.coords();
robust::Coord3D { x, y, z }
}
fn p3(x: f64, y: f64, z: f64) -> Point3 {
Point3::new(x, y, z).unwrap()
}
/// Uniform points in the unit cube.
fn random_cloud(r: &mut Rng, n: usize) -> Vec<Point3> {
(0..n).map(|_| p3(r.unit(), r.unit(), r.unit())).collect()
}
/// Vertices of a tessellated CAD part: a 100 × 60 × 40 mm box with 5 mm grid faces and a
/// Ø30 cylinder boss (64 segments, 8 rings) on top. Coplanar and cospherical subsets abound.
fn cad_cloud() -> Vec<Point3> {
let mut v = Vec::new();
let (lx, ly, lz, h) = (100.0, 60.0, 40.0, 5.0);
let steps = |l: f64| (0..=(l / h) as usize).map(move |i| i as f64 * h);
for x in steps(lx) {
for y in steps(ly) {
v.push(p3(x, y, 0.0));
v.push(p3(x, y, lz));
}
for z in steps(lz) {
v.push(p3(x, 0.0, z));
v.push(p3(x, ly, z));
}
}
for y in steps(ly) {
for z in steps(lz) {
v.push(p3(0.0, y, z));
v.push(p3(lx, y, z));
}
}
for ring in 0..8 {
for k in 0..64 {
let t = k as f64 * std::f64::consts::TAU / 64.0;
v.push(p3(50.0 + 15.0 * t.cos(), 30.0 + 15.0 * t.sin(), lz + ring as f64 * 2.5));
}
}
v
}
fn queries<const N: usize>(r: &mut Rng, cloud: &[Point3], n: usize) -> Vec<[Point3; N]> {
(0..n).map(|_| std::array::from_fn(|_| cloud[r.index(cloud.len())])).collect()
}
fn time<T>(label: &str, items: &[T], mut f: impl FnMut(&T) -> Sign) {
let start = Instant::now();
let mut acc = 0i64;
for _ in 0..5 {
for it in items {
acc += f(it) as i64;
}
}
let ns = start.elapsed().as_nanos() as f64 / (5 * items.len()) as f64;
println!(" {label:<34} {ns:7.1} ns/call");
black_box(acc);
}
fn hit_rate<T>(items: &[T], f: impl Fn(&T) -> Option<Sign>) -> f64 {
100.0 * items.iter().filter(|it| f(it).is_some()).count() as f64 / items.len() as f64
}
#[test]
#[ignore = "benchmark: run in release with --ignored --nocapture"]
fn predicate_throughput_and_filter_hit_rate() {
let mut r = Rng(42);
let n = 200_000;
for (name, cloud) in [("random unit cube", random_cloud(&mut r, 10_000)), ("CAD tessellation", cad_cloud())] {
let o: Vec<[Point3; 4]> = queries(&mut r, &cloud, n);
let s: Vec<[Point3; 5]> = queries(&mut r, &cloud, n);
println!("{name} ({} points, {n} queries)", cloud.len());
println!(
" filter hit rate: orient3d {:.2}%, insphere {:.2}%",
hit_rate(&o, |[a, b, c, d]| orient3d_filter(a, b, c, d)),
hit_rate(&s, |[a, b, c, d, e]| insphere_filter(a, b, c, d, e))
);
time("orient3d", &o, |[a, b, c, d]| orient3d(a, b, c, d));
time("orient3d (robust crate)", &o, |[a, b, c, d]| {
let k = |p: &Point3| {
let [x, y, z] = p.coords();
robust::Coord3D { x, y, z }
};
Sign::of(robust::orient3d(k(a), k(b), k(c), k(d)))
});
time("orient3d naive f64 (inexact)", &o, |[a, b, c, d]| {
let ([ax, ay, az], [bx, by, bz], [cx, cy, cz], [dx, dy, dz]) = (a.coords(), b.coords(), c.coords(), d.coords());
let (adx, ady, adz, bdx, bdy, bdz, cdx, cdy, cdz) = (ax - dx, ay - dy, az - dz, bx - dx, by - dy, bz - dz, cx - dx, cy - dy, cz - dz);
Sign::of(adz * (bdx * cdy - cdx * bdy) + bdz * (cdx * ady - adx * cdy) + cdz * (adx * bdy - bdx * ady))
});
time("insphere", &s, |[a, b, c, d, e]| insphere(a, b, c, d, e));
time("insphere (robust crate)", &s, |[a, b, c, d, e]| {
let k = |p: &Point3| {
let [x, y, z] = p.coords();
robust::Coord3D { x, y, z }
};
Sign::of(robust::insphere(k(a), k(b), k(c), k(d), k(e)))
});
time("insphere_sos", &s, |[a, b, c, d, e]| insphere_sos(a, b, c, d, e));
}
// Fully degenerate CAD queries: four vertices of one planar face (orient3d = 0), and five
// grid vertices of one face (insphere = 0 when not all coplanar is impossible: use the
// cylinder rings, which are cospherical in groups of points on two rings).
let face: Vec<Point3> = (0..=20).flat_map(|i| (0..=12).map(move |j| p3(i as f64 * 5.0, j as f64 * 5.0, 40.0))).collect();
let o: Vec<[Point3; 4]> = queries(&mut r, &face, n);
let ring = |z: f64| -> Vec<Point3> {
(0..64)
.map(|k| {
let t = k as f64 * std::f64::consts::TAU / 64.0;
p3(50.0 + 15.0 * t.cos(), 30.0 + 15.0 * t.sin(), z)
})
.collect()
};
let (r0, r1) = (ring(40.0), ring(42.5));
let s: Vec<[Point3; 5]> = (0..n)
.map(|i| {
let mut q: [Point3; 5] = std::array::from_fn(|k| if k < 3 { r0[r.index(64)] } else { r1[r.index(64)] });
// Same angle on both rings: symmetric configurations that are exactly cospherical.
if i % 2 == 0 {
let k = r.index(64);
q[3] = r1[k];
q[4] = r1[(k + 32) % 64];
}
q
})
.collect();
println!("degenerate CAD queries ({n} each)");
println!(
" filter hit rate: coplanar orient3d {:.2}%, ring insphere {:.2}%",
hit_rate(&o, |[a, b, c, d]| orient3d_filter(a, b, c, d)),
hit_rate(&s, |[a, b, c, d, e]| insphere_filter(a, b, c, d, e))
);
time("orient3d (coplanar face)", &o, |[a, b, c, d]| orient3d(a, b, c, d));
time("insphere (two cylinder rings)", &s, |[a, b, c, d, e]| insphere(a, b, c, d, e));
time("insphere_sos (two cylinder rings)", &s, |[a, b, c, d, e]| insphere_sos(a, b, c, d, e));
time("insphere (robust crate, rings)", &s, |[a, b, c, d, e]| Sign::of(robust::insphere(k3(a), k3(b), k3(c), k3(d), k3(e))));
time("orient3d (robust crate, face)", &o, |[a, b, c, d]| Sign::of(robust::orient3d(k3(a), k3(b), k3(c), k3(d))));
// Indirect: orient3d with one line-plane intersection and three explicit points.
let cloud = random_cloud(&mut r, 1000);
let mut items = Vec::new();
while items.len() < 20_000 {
let q: [Point3; 8] = std::array::from_fn(|_| cloud[r.index(cloud.len())]);
if let Ok(l) = Lpi::new(&q[0], &q[1], &q[2], &q[3], &q[4]) {
items.push([GenericPoint::Lpi(l), q[5].into(), q[6].into(), q[7].into()]);
}
}
println!("indirect (20000 queries)");
time("generic_orient3d (LPI + 3 explicit)", &items, |[a, b, c, d]| generic_orient3d(a, b, c, d));
}