//! Differential tests against independent oracles: exact rationals (`num-rational`, Leibniz //! determinants written here from the definitions), `i128` on integer grids, and georust's //! `robust` (a port of Shewchuk's public-domain `predicates.c`). use std::cmp::Ordering; use num_bigint::BigInt; use num_rational::BigRational; use num_traits::{FromPrimitive, Signed as _, ToPrimitive, Zero}; use super::adaptive::exact; use super::*; // --------------------------------------------------------------------------------------------- // Oracles and generators // --------------------------------------------------------------------------------------------- fn q(x: f64) -> BigRational { BigRational::from_f64(x).unwrap() } fn rsign(x: &BigRational) -> Sign { if x.is_zero() { Sign::Zero } else if x.is_positive() { Sign::Positive } else { Sign::Negative } } /// Laplace expansion along the first row. fn rdet(m: &[Vec]) -> BigRational { if m.len() == 1 { return m[0][0].clone(); } let mut total = BigRational::zero(); for j in 0..m.len() { let minor: Vec> = m[1..].iter().map(|row| row.iter().enumerate().filter(|(k, _)| *k != j).map(|(_, v)| v.clone()).collect()).collect(); let term = m[0][j].clone() * rdet(&minor); if j % 2 == 0 { total += term } else { total -= term } } total } fn rdiff(p: &[f64], o: &[f64]) -> Vec { p.iter().zip(o).map(|(a, b)| q(*a) - q(*b)).collect() } fn lifted(mut row: Vec) -> Vec { let lift = row.iter().fold(BigRational::zero(), |acc, v| acc + v.clone() * v.clone()); row.push(lift); row } fn o2(a: &Point2, b: &Point2, c: &Point2) -> Sign { rsign(&rdet(&[rdiff(&a.0, &c.0), rdiff(&b.0, &c.0)])) } fn o3(a: &Point3, b: &Point3, c: &Point3, d: &Point3) -> Sign { rsign(&rdet(&[rdiff(&a.0, &d.0), rdiff(&b.0, &d.0), rdiff(&c.0, &d.0)])) } fn ic(a: &Point2, b: &Point2, c: &Point2, d: &Point2) -> Sign { rsign(&rdet(&[lifted(rdiff(&a.0, &d.0)), lifted(rdiff(&b.0, &d.0)), lifted(rdiff(&c.0, &d.0))])) } fn is(a: &Point3, b: &Point3, c: &Point3, d: &Point3, e: &Point3) -> Sign { rsign(&rdet(&[lifted(rdiff(&a.0, &e.0)), lifted(rdiff(&b.0, &e.0)), lifted(rdiff(&c.0, &e.0)), lifted(rdiff(&d.0, &e.0))])) } /// SplitMix64: a deterministic generator for reproducible tests. 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) } /// Uniform in [-1, 1). fn unit(&mut self) -> f64 { (self.next_u64() >> 11) as f64 / (1u64 << 52) as f64 - 1.0 } fn int(&mut self, n: i64) -> i64 { (self.next_u64() % (2 * n as u64 + 1)) as i64 - n } /// `x` moved by up to `k` ulps either way. fn nudge(&mut self, mut x: f64, k: i64) -> f64 { let steps = self.int(k); for _ in 0..steps.abs() { x = if steps > 0 { x.next_up() } else { x.next_down() }; } x } } fn p2(x: f64, y: f64) -> Point2 { Point2::new(x, y).unwrap() } fn p3(x: f64, y: f64, z: f64) -> Point3 { Point3::new(x, y, z).unwrap() } fn rc2(p: &Point2) -> robust::Coord { robust::Coord { x: p.0[0], y: p.0[1] } } fn rc3(p: &Point3) -> robust::Coord3D { robust::Coord3D { x: p.0[0], y: p.0[1], z: p.0[2] } } /// Every stage agrees with every oracle, and the filter, when it answers, agrees too. fn check2(a: &Point2, b: &Point2, c: &Point2, d: &Point2) { let want = o2(a, b, c); assert_eq!(orient2d(a, b, c), want, "orient2d {a:?} {b:?} {c:?}"); assert_eq!(exact::orient2d(a, b, c), want); assert_eq!(Sign::of(robust::orient2d(rc2(a), rc2(b), rc2(c))), want); assert!(orient2d_filter(a, b, c).is_none_or(|s| s == want)); let want = ic(a, b, c, d); assert_eq!(incircle(a, b, c, d), want, "incircle {a:?} {b:?} {c:?} {d:?}"); assert_eq!(exact::incircle(a, b, c, d), want); assert_eq!(Sign::of(robust::incircle(rc2(a), rc2(b), rc2(c), rc2(d))), want); assert!(incircle_filter(a, b, c, d).is_none_or(|s| s == want)); } fn check3(a: &Point3, b: &Point3, c: &Point3, d: &Point3, e: &Point3) { let want = o3(a, b, c, d); assert_eq!(orient3d(a, b, c, d), want, "orient3d {a:?} {b:?} {c:?} {d:?}"); assert_eq!(exact::orient3d(a, b, c, d), want); assert_eq!(Sign::of(robust::orient3d(rc3(a), rc3(b), rc3(c), rc3(d))), want); assert!(orient3d_filter(a, b, c, d).is_none_or(|s| s == want)); let want = is(a, b, c, d, e); assert_eq!(insphere(a, b, c, d, e), want, "insphere {a:?} {b:?} {c:?} {d:?} {e:?}"); assert_eq!(exact::insphere(a, b, c, d, e), want); assert_eq!(Sign::of(robust::insphere(rc3(a), rc3(b), rc3(c), rc3(d), rc3(e))), want); assert!(insphere_filter(a, b, c, d, e).is_none_or(|s| s == want)); } // --------------------------------------------------------------------------------------------- // Explicit predicates // --------------------------------------------------------------------------------------------- #[test] fn random_points_match_the_oracles() { let mut r = Rng(1); for _ in 0..300 { let pt2 = |r: &mut Rng| p2(r.unit(), r.unit()); let (a, b, c, d) = (pt2(&mut r), pt2(&mut r), pt2(&mut r), pt2(&mut r)); check2(&a, &b, &c, &d); let pt3 = |r: &mut Rng| p3(r.unit(), r.unit(), r.unit()); let (a, b, c, d, e) = (pt3(&mut r), pt3(&mut r), pt3(&mut r), pt3(&mut r), pt3(&mut r)); check3(&a, &b, &c, &d, &e); } } #[test] fn near_degenerate_points_match_the_oracles() { // Points on a line, plane, circle or sphere, rounded and then moved by a few ulps: the // filter fails and every adaptive stage is exercised. let mut r = Rng(2); for i in 0..400 { let k = if i % 4 == 0 { 0 } else { 2 }; let scale = [1.0, 1e-3, 1e5, 3.0e-20][i % 4]; let (ax, ay, bx, by) = (r.unit() * scale, r.unit() * scale, r.unit() * scale, r.unit() * scale); let t = r.unit(); let c = p2(r.nudge(ax + t * (bx - ax), k), r.nudge(ay + t * (by - ay), k)); let (a, b) = (p2(ax, ay), p2(bx, by)); let on_circle = |r: &mut Rng| { let th = r.unit() * 3.2; p2(r.nudge(scale * th.cos() + scale * 0.25, k), r.nudge(scale * th.sin() - scale * 0.5, k)) }; let (ca, cb, cc, cd) = (on_circle(&mut r), on_circle(&mut r), on_circle(&mut r), on_circle(&mut r)); check2(&a, &b, &c, &c); check2(&ca, &cb, &cc, &cd); let pt = |r: &mut Rng| [r.unit() * scale, r.unit() * scale, r.unit() * scale]; let (a, b, c) = (pt(&mut r), pt(&mut r), pt(&mut r)); let (s, t) = (r.unit(), r.unit()); let d: Vec = (0..3).map(|j| r.nudge(a[j] + s * (b[j] - a[j]) + t * (c[j] - a[j]), k)).collect(); let on_sphere = |r: &mut Rng| { let (th, ph) = (r.unit() * 3.2, r.unit() * 1.6); p3(r.nudge(scale * ph.cos() * th.cos() + scale, k), r.nudge(scale * ph.cos() * th.sin(), k), r.nudge(scale * ph.sin() - scale * 0.5, k)) }; let (a, b, c, d) = (p3(a[0], a[1], a[2]), p3(b[0], b[1], b[2]), p3(c[0], c[1], c[2]), p3(d[0], d[1], d[2])); let s: Vec = (0..5).map(|_| on_sphere(&mut r)).collect(); check3(&a, &b, &c, &d, &s[0]); check3(&s[0], &s[1], &s[2], &s[3], &s[4]); } } #[test] fn ulp_grid_near_a_line_matches_the_oracle() { // The classic failure of naive orient2d (Kettner et al., "Classroom examples of robustness // problems"): p on a 64×64 grid of ulps around (0.5, 0.5), q = (12, 12), r = (24, 24). // Variants with points off the diagonal make the coordinate differences inexact. let ulp = 2f64.powi(-53); for (q2, r2) in [(p2(12.0, 12.0), p2(24.0, 24.0)), (p2(0.1, 0.3), p2(17.3, 0.7)), (p2(1e-9, 3.0), p2(0.9, 1e-7))] { for i in 0..64 { for j in 0..64 { let p = p2(0.5 + i as f64 * ulp, 0.5 + j as f64 * ulp); let want = o2(&p, &q2, &r2); assert_eq!(orient2d(&p, &q2, &r2), want); assert_eq!(orient2d(&q2, &r2, &p), want); assert_eq!(orient2d(&r2, &p, &q2), want); } } } } fn i128_det(m: &[Vec]) -> i128 { if m.len() == 1 { return m[0][0]; } (0..m.len()) .map(|j| { let minor: Vec> = m[1..].iter().map(|row| row.iter().enumerate().filter(|(k, _)| *k != j).map(|(_, v)| *v).collect()).collect(); let t = m[0][j] * i128_det(&minor); if j % 2 == 0 { t } else { -t } }) .sum() } #[test] fn integer_grids_match_i128() { // Small grids are full of exact degeneracies (collinear, coplanar, cocircular, // cospherical); large coordinates (2^30 for orient, 2^14 for in-sphere) stress the filter. let mut r = Rng(3); for &(n, big) in &[(2i64, 1i64), (3, 1 << 30), (3, 1 << 14)] { for _ in 0..400 { let v: Vec<[i64; 3]> = (0..5).map(|_| [r.int(n) * big, r.int(n) * big, r.int(n) * big]).collect(); let pt = |p: &[i64; 3]| p3(p[0] as f64, p[1] as f64, p[2] as f64); let d = |p: &[i64; 3], o: &[i64; 3]| (0..3).map(|k| (p[k] - o[k]) as i128).collect::>(); let o3 = i128_det(&[d(&v[0], &v[3]), d(&v[1], &v[3]), d(&v[2], &v[3])]); assert_eq!(orient3d(&pt(&v[0]), &pt(&v[1]), &pt(&v[2]), &pt(&v[3])), Sign::of(o3 as f64)); let o2v = |p: &[i64; 3]| p2(p[0] as f64, p[1] as f64); let d2 = |p: &[i64; 3], o: &[i64; 3]| d(p, o)[..2].to_vec(); let want = i128_det(&[d2(&v[0], &v[2]), d2(&v[1], &v[2])]); assert_eq!(orient2d(&o2v(&v[0]), &o2v(&v[1]), &o2v(&v[2])), Sign::of(want as f64)); if big <= 1 << 14 || n == 2 { let lift = |mut row: Vec| { row.push(row.iter().map(|x| x * x).sum()); row }; let s = i128_det(&[lift(d(&v[0], &v[4])), lift(d(&v[1], &v[4])), lift(d(&v[2], &v[4])), lift(d(&v[3], &v[4]))]); assert_eq!(insphere(&pt(&v[0]), &pt(&v[1]), &pt(&v[2]), &pt(&v[3]), &pt(&v[4])), Sign::of(s as f64)); let c = i128_det(&[lift(d2(&v[0], &v[3])), lift(d2(&v[1], &v[3])), lift(d2(&v[2], &v[3]))]); assert_eq!(incircle(&o2v(&v[0]), &o2v(&v[1]), &o2v(&v[2]), &o2v(&v[3])), Sign::of(c as f64)); } } } } #[test] fn extreme_magnitudes_inside_the_domain_stay_exact() { // Coordinates at both ends of the domain, mixed, near-degenerate: no overflow or underflow // may corrupt a sign. let mut r = Rng(4); let tiny = MIN_MAGNITUDE; let huge = MAX_MAGNITUDE; for i in 0..150 { let s = [tiny, huge, 1.0, tiny * 1e10][i % 4]; let pick = |r: &mut Rng| { let m = r.unit(); if m.abs() < 0.2 { 0.0 } else { (m * s).clamp(-huge, huge) } }; let fix = |x: f64| if is_admissible(x) { x } else { 0.0 }; let v: Vec = (0..5) .map(|_| { let (x, y, z) = (pick(&mut r), pick(&mut r), pick(&mut r)); p3(fix(r.nudge(x, 1)), fix(y), fix(z)) }) .collect(); check3(&v[0], &v[1], &v[2], &v[3], &v[4]); // Collinear and coplanar with huge and tiny offsets mixed. let base = p3(huge / 2.0, tiny, 0.0); let shifted = p3(huge / 2.0, tiny * 2.0, tiny); check3(&base, &shifted, &v[2], &v[3], &v[4]); } } #[test] fn permutations_follow_the_determinant() { let mut r = Rng(5); for _ in 0..200 { // Grid points so that zero results also occur. let pt = |r: &mut Rng| p3(r.int(2) as f64, r.int(2) as f64, r.int(2) as f64 * 0.5); let (a, b, c, d, e) = (pt(&mut r), pt(&mut r), pt(&mut r), pt(&mut r), pt(&mut r)); let s = orient3d(&a, &b, &c, &d); assert_eq!(orient3d(&b, &a, &c, &d), -s); assert_eq!(orient3d(&a, &b, &d, &c), -s); assert_eq!(orient3d(&b, &c, &a, &d), s); assert_eq!(orient3d(&b, &c, &d, &a), -s); // a 4-cycle is odd let s = insphere(&a, &b, &c, &d, &e); assert_eq!(insphere(&b, &a, &c, &d, &e), -s); assert_eq!(insphere(&a, &b, &c, &e, &d), -s); assert_eq!(insphere(&c, &a, &b, &d, &e), s); let (a2, b2, c2, d2) = (p2(a.0[0], a.0[1]), p2(b.0[0], b.0[1]), p2(c.0[0], c.0[1]), p2(d.0[0], d.0[2])); let s = orient2d(&a2, &b2, &c2); assert_eq!(orient2d(&b2, &a2, &c2), -s); assert_eq!(orient2d(&b2, &c2, &a2), s); let s = incircle(&a2, &b2, &c2, &d2); assert_eq!(incircle(&b2, &a2, &c2, &d2), -s); assert_eq!(incircle(&a2, &b2, &d2, &c2), -s); } } #[test] fn conventions_match_the_documentation() { let (o, x, y, z) = (p3(0.0, 0.0, 0.0), p3(1.0, 0.0, 0.0), p3(0.0, 1.0, 0.0), p3(0.0, 0.0, 1.0)); // o, x, y counterclockwise seen from +z; a point below gives Positive. assert_eq!(orient3d(&o, &x, &y, &p3(0.0, 0.0, -1.0)), Sign::Positive); assert_eq!(orient3d(&o, &x, &y, &z), Sign::Negative); let tet = [o, x, y, p3(0.0, 0.0, -1.0)]; assert_eq!(insphere(&tet[0], &tet[1], &tet[2], &tet[3], &p3(0.1, 0.1, -0.1)), Sign::Positive); assert_eq!(insphere(&tet[0], &tet[1], &tet[2], &tet[3], &p3(5.0, 5.0, 5.0)), Sign::Negative); assert_eq!(insphere(&tet[0], &tet[1], &tet[2], &tet[3], &p3(1.0, 1.0, 0.0)), Sign::Zero); let (a, b, c) = (p2(0.0, 0.0), p2(1.0, 0.0), p2(0.0, 1.0)); assert_eq!(orient2d(&a, &b, &c), Sign::Positive); assert_eq!(incircle(&a, &b, &c, &p2(0.5, 0.5)), Sign::Positive); assert_eq!(incircle(&a, &b, &c, &p2(1.0, 1.0)), Sign::Zero); } #[test] fn the_domain_is_enforced_at_construction() { for bad in [f64::NAN, f64::INFINITY, -f64::INFINITY, 1e-40, -1e-300, 5e-324, 1e49, f64::MAX] { assert!(Point3::new(0.0, bad, 1.0).is_err(), "{bad}"); assert!(Point2::try_from([bad, 0.0]).is_err(), "{bad}"); } for good in [0.0, -0.0, MIN_MAGNITUDE, -MAX_MAGNITUDE, 1e-36, 1e48] { assert!(Point3::new(good, good, good).is_ok(), "{good}"); } } #[test] fn hostile_bit_patterns_never_panic() { let mut r = Rng(6); let mut accepted = Vec::new(); for _ in 0..20_000 { let c = [f64::from_bits(r.next_u64()), f64::from_bits(r.next_u64()), f64::from_bits(r.next_u64())]; if let Ok(p) = Point3::try_from(c) { accepted.push(p); } } accepted.push(p3(0.0, 0.0, 0.0)); for w in accepted.windows(5).take(200) { let s = orient3d(&w[0], &w[1], &w[2], &w[3]); assert_eq!(s, o3(&w[0], &w[1], &w[2], &w[3])); let s = insphere(&w[0], &w[1], &w[2], &w[3], &w[4]); assert_eq!(s, is(&w[0], &w[1], &w[2], &w[3], &w[4])); let _ = insphere_sos(&w[0], &w[1], &w[2], &w[3], &w[4]); } } // --------------------------------------------------------------------------------------------- // Symbolic perturbation // --------------------------------------------------------------------------------------------- /// The perturbed determinant evaluated with an actual tiny ε, lifts `|p|² + ε^rank` (rank 1 for /// the lexicographically largest point): an independent check of the cofactor signs. fn perturbed_sign(points: &[Vec]) -> Sign { let mut order: Vec = (0..points.len()).collect(); order.sort_by(|&i, &j| points[j].partial_cmp(&points[i]).unwrap_or(Ordering::Equal)); let eps = BigRational::new(BigInt::from(1), BigInt::from(1u64 << 40)); let rows: Vec> = points .iter() .enumerate() .map(|(i, p)| { let rank = order.iter().position(|&k| k == i).unwrap() + 1; let mut row = lifted(p.iter().map(|x| q(*x)).collect()); let mut e = BigRational::from_integer(BigInt::from(1)); for _ in 0..rank { e *= eps.clone(); } if let Some(lift) = row.last_mut() { *lift += e; } row.push(BigRational::from_integer(BigInt::from(1))); row }) .collect(); rsign(&rdet(&rows)) } #[test] fn perturbed_insphere_matches_an_infinitesimal_lift() { let mut r = Rng(7); let mut zeros_resolved = 0; for _ in 0..600 { let v: Vec> = (0..5).map(|_| vec![r.int(1) as f64, r.int(1) as f64, r.int(1) as f64]).collect(); if (0..5).any(|i| (0..i).any(|j| v[i] == v[j])) { continue; } let p: Vec = v.iter().map(|c| p3(c[0], c[1], c[2])).collect(); if orient3d(&p[0], &p[1], &p[2], &p[3]).is_zero() { continue; } let got = insphere_sos(&p[0], &p[1], &p[2], &p[3], &p[4]); assert_ne!(got, Sign::Zero); assert_eq!(got, perturbed_sign(&v), "{v:?}"); zeros_resolved += insphere(&p[0], &p[1], &p[2], &p[3], &p[4]).is_zero() as usize; } assert!(zeros_resolved > 20, "the grid must produce cospherical cases ({zeros_resolved})"); } #[test] fn perturbed_incircle_matches_an_infinitesimal_lift() { let mut r = Rng(8); let mut zeros_resolved = 0; for _ in 0..600 { let v: Vec> = (0..4).map(|_| vec![r.int(1) as f64, r.int(1) as f64]).collect(); if (0..4).any(|i| (0..i).any(|j| v[i] == v[j])) { continue; } let p: Vec = v.iter().map(|c| p2(c[0], c[1])).collect(); if orient2d(&p[0], &p[1], &p[2]).is_zero() { continue; } let got = incircle_sos(&p[0], &p[1], &p[2], &p[3]); assert_ne!(got, Sign::Zero); assert_eq!(got, perturbed_sign(&v), "{v:?}"); zeros_resolved += incircle(&p[0], &p[1], &p[2], &p[3]).is_zero() as usize; } assert!(zeros_resolved > 20, "{zeros_resolved}"); } // --------------------------------------------------------------------------------------------- // Indirect predicates // --------------------------------------------------------------------------------------------- type RPoint = [BigRational; 3]; fn rp(p: &Point3) -> RPoint { p.0.map(q) } fn rsub(a: &RPoint, b: &RPoint) -> RPoint { std::array::from_fn(|k| a[k].clone() - b[k].clone()) } fn rdot(a: &RPoint, b: &RPoint) -> BigRational { (0..3).fold(BigRational::zero(), |acc, k| acc + a[k].clone() * b[k].clone()) } fn rcross(a: &RPoint, b: &RPoint) -> RPoint { [ a[1].clone() * b[2].clone() - a[2].clone() * b[1].clone(), a[2].clone() * b[0].clone() - a[0].clone() * b[2].clone(), a[0].clone() * b[1].clone() - a[1].clone() * b[0].clone(), ] } /// Line–plane intersection by the parametric form (independent of the determinant formula). fn r_lpi(q1: &Point3, q2: &Point3, r: &Point3, s: &Point3, t: &Point3) -> RPoint { let (q1, q2, r, s, t) = (rp(q1), rp(q2), rp(r), rp(s), rp(t)); let n = rcross(&rsub(&s, &r), &rsub(&t, &r)); let dir = rsub(&q2, &q1); let u = rdot(&n, &rsub(&r, &q1)) / rdot(&n, &dir); std::array::from_fn(|k| q1[k].clone() + u.clone() * dir[k].clone()) } /// Three-plane intersection by Gaussian elimination. fn r_tpi(planes: &[[Point3; 3]; 3]) -> RPoint { let mut m: Vec> = planes .iter() .map(|[r, s, t]| { let (r, s, t) = (rp(r), rp(s), rp(t)); let n = rcross(&rsub(&s, &r), &rsub(&t, &r)); let c = rdot(&n, &r); vec![n[0].clone(), n[1].clone(), n[2].clone(), c] }) .collect(); for col in 0..3 { let piv = (col..3).find(|&i| !m[i][col].is_zero()).unwrap(); m.swap(col, piv); for i in 0..3 { if i != col { let f = m[i][col].clone() / m[col][col].clone(); let pivot_row = m[col].clone(); for (cell, p) in m[i].iter_mut().zip(&pivot_row) { *cell -= p.clone() * f.clone(); } } } } std::array::from_fn(|k| m[k][3].clone() / m[k][k].clone()) } fn r_orient3d(p: &[RPoint; 4]) -> Sign { let rows: Vec> = (0..3).map(|i| rsub(&p[i], &p[3]).to_vec()).collect(); rsign(&rdet(&rows)) } fn rand_pt(r: &mut Rng, scale: f64) -> Point3 { p3(r.unit() * scale, r.unit() * scale, r.unit() * scale) } #[test] fn indirect_orient3d_matches_rational_constructions() { let mut r = Rng(9); let mut checked = 0; for i in 0..150 { let pts: Vec = (0..14).map(|_| rand_pt(&mut r, 1.0)).collect(); let Ok(lpi) = Lpi::new(&pts[0], &pts[1], &pts[2], &pts[3], &pts[4]) else { continue }; let Ok(tpi) = Tpi::new([[&pts[5], &pts[6], &pts[7]], [&pts[8], &pts[9], &pts[10]], [&pts[11], &pts[12], &pts[13]]]) else { continue; }; let rl = r_lpi(&pts[0], &pts[1], &pts[2], &pts[3], &pts[4]); let rt = r_tpi(&[[pts[5], pts[6], pts[7]], [pts[8], pts[9], pts[10]], [pts[11], pts[12], pts[13]]]); let g = [GenericPoint::Lpi(lpi.clone()), GenericPoint::Tpi(tpi.clone())]; let rg = [rl.clone(), rt.clone()]; let ex: Vec = (0..4).map(|_| rand_pt(&mut r, 1.0)).collect(); // Every mix of implicit and explicit points in every position. for mask in 0u32..81 { let mut gp = Vec::new(); let mut rpv = Vec::new(); let mut m = mask; for e in &ex { match m % 3 { 0 => { gp.push(GenericPoint::Explicit(*e)); rpv.push(rp(e)); } k => { gp.push(g[k as usize - 1].clone()); rpv.push(rg[k as usize - 1].clone()); } } m /= 3; } if !(i + mask as usize).is_multiple_of(7) { continue; // a deterministic sample of the masks keeps the test fast } let want = r_orient3d(&[rpv[0].clone(), rpv[1].clone(), rpv[2].clone(), rpv[3].clone()]); assert_eq!(generic_orient3d(&gp[0], &gp[1], &gp[2], &gp[3]), want, "mask {mask}"); checked += 1; } // The constructed points lie exactly on their defining planes. let lp = GenericPoint::Lpi(lpi); let tp = GenericPoint::Tpi(tpi); let e = |p: &Point3| GenericPoint::Explicit(*p); assert_eq!(generic_orient3d(&e(&pts[2]), &e(&pts[3]), &e(&pts[4]), &lp), Sign::Zero); assert_eq!(generic_orient3d(&lp, &e(&pts[2]), &e(&pts[3]), &e(&pts[4])), Sign::Zero); for k in 0..3 { let pl = &pts[5 + 3 * k..8 + 3 * k]; assert_eq!(generic_orient3d(&e(&pl[0]), &e(&pl[1]), &tp, &e(&pl[2])), Sign::Zero); } // ... and on their line. for axis in [Axis::X, Axis::Y, Axis::Z] { assert_eq!(generic_orient2d(&e(&pts[0]), &e(&pts[1]), &lp, axis), Sign::Zero); } // Two different constructions of the same point compare equal on every axis. let again = GenericPoint::Lpi(Lpi::new(&pts[1], &pts[0], &pts[3], &pts[4], &pts[2]).unwrap()); for axis in [Axis::X, Axis::Y, Axis::Z] { assert_eq!(generic_compare(&lp, &again, axis), Ordering::Equal); let want = rl[axis.index()].cmp(&rt[axis.index()]); assert_eq!(generic_compare(&lp, &tp, axis), want); let want = rl[axis.index()].cmp(&q(ex[0].0[axis.index()])); assert_eq!(generic_compare(&lp, &e(&ex[0]), axis), want); } let approx = lp.approx(); for k in 0..3 { assert!((approx[k] - rl[k].to_f64().unwrap()).abs() <= 1e-9 * (1.0 + approx[k].abs())); } } assert!(checked > 500, "{checked}"); } #[test] fn indirect_orient2d_matches_rational_constructions() { let mut r = Rng(10); for _ in 0..200 { let pts: Vec = (0..8).map(|_| rand_pt(&mut r, 10.0)).collect(); let Ok(lpi) = Lpi::new(&pts[0], &pts[1], &pts[2], &pts[3], &pts[4]) else { continue }; let rl = r_lpi(&pts[0], &pts[1], &pts[2], &pts[3], &pts[4]); for axis in [Axis::X, Axis::Y, Axis::Z] { let (u, v) = axis.projected(); for slot in 0..3 { let mut g: Vec = pts[5..8].iter().map(|p| GenericPoint::Explicit(*p)).collect(); let mut rr: Vec = pts[5..8].iter().map(rp).collect(); g[slot] = GenericPoint::Lpi(lpi.clone()); rr[slot] = rl.clone(); let d = |p: &RPoint, o: &RPoint| vec![p[u].clone() - o[u].clone(), p[v].clone() - o[v].clone()]; let want = rsign(&rdet(&[d(&rr[0], &rr[2]), d(&rr[1], &rr[2])])); assert_eq!(generic_orient2d(&g[0], &g[1], &g[2], axis), want); } } } } #[test] fn undefined_constructions_are_rejected() { let o = p3(0.0, 0.0, 0.0); let (x, y, z) = (p3(1.0, 0.0, 0.0), p3(0.0, 1.0, 0.0), p3(0.0, 0.0, 1.0)); // Line along x, plane z = 0 containing it: parallel. assert!(Lpi::new(&o, &x, &o, &x, &y).is_err()); // Degenerate line. assert!(Lpi::new(&z, &z, &o, &x, &y).is_err()); // Collinear plane points. assert!(Lpi::new(&o, &z, &o, &x, &p3(2.0, 0.0, 0.0)).is_err()); // A nearly parallel line is fine: z rises by one ulp over the segment. assert!(Lpi::new(&p3(0.0, 0.0, -1.0), &p3(1.0, 0.0, -1.0 + f64::EPSILON), &o, &x, &y).is_ok()); // Two parallel planes. let lifted = |p: &Point3| p3(p.0[0], p.0[1], 1.0); assert!(Tpi::new([[&o, &x, &y], [&lifted(&o), &lifted(&x), &lifted(&y)], [&o, &x, &z]]).is_err()); let t = Tpi::new([[&o, &x, &y], [&o, &y, &z], [&o, &z, &x]]).unwrap(); let t = GenericPoint::Tpi(t); for axis in [Axis::X, Axis::Y, Axis::Z] { assert_eq!(generic_compare(&t, &GenericPoint::Explicit(o), axis), Ordering::Equal); } }