Count the contact grid's own work, and test find_close_pair (#836)

find_close_pair's budget now counts the grid's own work too (each cell a triangle is filed under or visits), not only the pair tests, so a pathological pair of meshes can't spend unbounded time building and walking the grid. Adds tests for find_close_pair and the budget.
This commit is contained in:
Giovanni authored and GitHub committed 2026-10-13 20:03:05 -04:00
1 parent 7eac1de9ee
commit fdaf57daa3
3 files changed
+224 -2

No files matched your search

+145
View File
@@ -319,3 +319,148 @@ fn meshes_cross_only_where_their_surfaces_pass_through_each_other() {
assert!(!a.crosses(&cube(9.995, 3.0, 1), 0.01));
assert!(!a.crosses(&cube(9.85, 3.0, 1), f64::NAN));
}
mod close_pairs {
use super::*;
/// A small deterministic generator, so the random cases below repeat from run to run.
struct Lcg(u64);
impl Lcg {
fn unit(&mut self) -> f64 {
self.0 = self.0.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
(self.0 >> 11) as f64 / (1u64 << 53) as f64
}
fn point(&mut self) -> Vec3 {
Vec3::new(self.unit(), self.unit(), self.unit())
}
}
fn boxed(tri: [Vec3; 3]) -> BoxedTriangle {
let mut bounds = Aabb3::EMPTY;
for p in tri {
bounds.add(p);
}
(tri, bounds)
}
/// `count` small triangles scattered over a cube of side `spread` at `origin`. On the lattice their
/// corners sit on whole numbers, so many touch exactly, share edges or lie in one plane.
fn scatter(rng: &mut Lcg, count: usize, origin: Vec3, spread: f64, size: f64, lattice: bool) -> Vec<BoxedTriangle> {
let mut out = Vec::with_capacity(count);
for _ in 0..count {
let centre = origin + rng.point() * spread;
let mut tri = [Vec3::ZERO; 3];
for corner in &mut tri {
let mut p = centre + (rng.point() - Vec3::new(0.5, 0.5, 0.5)) * (2.0 * size);
if lattice {
p = Vec3::new(p.x.round(), p.y.round(), p.z.round());
}
*corner = p;
}
out.push(boxed(tri));
}
out
}
/// A pair that "meets" for these tests: two corners within `gap` of each other.
fn corners_meet(t: &[Vec3; 3], u: &[Vec3; 3], gap: f64) -> bool {
t.iter().any(|p| u.iter().any(|q| p.dist(*q) <= gap))
}
fn boxes_close(x: &Aabb3, y: &Aabb3, gap: f64) -> bool {
x.min.x <= y.max.x + gap
&& y.min.x <= x.max.x + gap
&& x.min.y <= y.max.y + gap
&& y.min.y <= x.max.y + gap
&& x.min.z <= y.max.z + gap
&& y.min.z <= x.max.z + gap
}
/// Every pair tried, which is how the contact tests worked before they used a grid.
fn every_pair(a: &[BoxedTriangle], b: &[BoxedTriangle], gap: f64) -> bool {
a.iter().any(|(x, xb)| b.iter().any(|(y, yb)| boxes_close(xb, yb, gap) && corners_meet(x, y, gap)))
}
/// A flat grid of `n` x `n` squares, two triangles each, at height `z`.
fn flat_grid(n: usize, z: f64) -> Vec<BoxedTriangle> {
let p = |i: usize, j: usize| Vec3::new(i as f64, j as f64, z);
let mut out = Vec::with_capacity(2 * n * n);
for i in 0..n {
for j in 0..n {
out.push(boxed([p(i, j), p(i + 1, j), p(i + 1, j + 1)]));
out.push(boxed([p(i, j), p(i + 1, j + 1), p(i, j + 1)]));
}
}
out
}
/// The grid hands `test` the pairs that trying every pair finds: on random triangles, on
/// triangles that touch exactly (whole-number corners) and far from the origin.
#[test]
fn the_grid_finds_the_pairs_that_trying_every_pair_finds() {
let mut rng = Lcg(0x232);
let (mut met, mut apart) = (0, 0);
for case in 0..600usize {
let lattice = case % 3 == 0;
let origin = if case % 5 == 0 { Vec3::new(1.0e6, -2.0e6, 3.0e6) } else { Vec3::ZERO };
let gap = [1e-9, 0.01, 0.2][(case / 3) % 3];
let (na, nb) = (1 + (rng.unit() * 150.0) as usize, 1 + (rng.unit() * 150.0) as usize);
let shift = (rng.point() - Vec3::new(0.5, 0.5, 0.5)) * 8.0;
let size = 0.3 + rng.unit();
let a = scatter(&mut rng, na, origin, 5.0, size, lattice);
let b = scatter(&mut rng, nb, origin + shift, 5.0, size, lattice);
let want = every_pair(&a, &b, gap);
let mut budget = usize::MAX;
let got = find_close_pair(&a, &b, gap, &mut budget, |(x, _), (y, _)| corners_meet(x, y, gap));
assert_eq!(got, Some(want), "case {case}: {na} x {nb} triangles, gap {gap}, lattice {lattice}");
if want {
met += 1;
} else {
apart += 1;
}
}
assert!(met > 50 && apart > 50, "the cases should cover both answers: {met} meet, {apart} apart");
let mut budget = usize::MAX;
assert_eq!(find_close_pair(&[], &flat_grid(2, 0.0), 0.1, &mut budget, |_, _| true), Some(false));
assert_eq!(find_close_pair(&flat_grid(2, 0.0), &[], 0.1, &mut budget, |_, _| true), Some(false));
}
/// Two flat meshes of 16,200 triangles each, one above the other: all their boxes overlap, which
/// is 262 million pairs and far past a budget of 40 million.
#[test]
fn big_meshes_with_overlapping_boxes_are_searched_without_trying_every_pair() {
let (low, high) = (flat_grid(90, 0.0), flat_grid(90, 0.5));
assert!(low.len() * high.len() > 200_000_000);
let mut budget = 40_000_000;
assert_eq!(find_close_pair(&low, &high, 0.1, &mut budget, |(x, _), (y, _)| corners_meet(x, y, 0.1)), Some(false));
let mut budget = 40_000_000;
assert_eq!(find_close_pair(&low, &high, 0.6, &mut budget, |(x, _), (y, _)| corners_meet(x, y, 0.6)), Some(true));
}
/// A pile of triangles in one place is still a product of pairs: the budget refuses it.
#[test]
fn a_pile_of_triangles_in_one_place_runs_out_of_budget() {
let pile = |height: f64| {
let up = Vec3::Z * height;
vec![boxed([up, Vec3::X + up, Vec3::Y + up]); 7000]
};
let (low, high) = (pile(0.0), pile(0.5));
let mut budget = 40_000_000;
// Their boxes are 0.5 apart, so within a gap of 1 every pair is a candidate: 49 million tests.
assert_eq!(find_close_pair(&low, &high, 1.0, &mut budget, |_, _| false), None);
}
/// The grid's own work counts: a budget too small to file a big mesh is refused even when no
/// pair is ever tested.
#[test]
fn filing_and_visiting_cells_count_against_the_budget() {
let (low, high) = (flat_grid(30, 0.0), flat_grid(30, 5.0));
let mut budget = 100;
assert_eq!(find_close_pair(&low, &high, 0.1, &mut budget, |_, _| false), None);
let mut budget = 10_000_000;
assert_eq!(find_close_pair(&low, &high, 0.1, &mut budget, |_, _| false), Some(false));
assert!(budget < 10_000_000, "work was counted");
}
}