Skip to main content

qualia_core_db/specialized_libs/computational_geometry/
insphere.rs

1//! `insphere` — the 3-D in-sphere predicate (P1.6).
2//!
3//! Computes the sign of the determinant that classifies whether a point `e`
4//! lies inside, on, or outside the oriented sphere through `a, b, c, d`.
5//!
6//! ## Sign convention (this implementation)
7//!
8//! **This implementation uses the opposite sign convention from the standard
9//! Shewchuk / de Berg formulation.** When `a, b, c, d` have positive
10//! orientation (positive [`super::orient3d::orient_3d`]), [`Sign::Negative`]
11//! means `e` is **inside** the sphere, [`Sign::Zero`] means on it, and
12//! [`Sign::Positive`] means **outside**. The sense is reversed when the
13//! orientation is negative (negative `orient_3d` → inside = `Positive`).
14//!
15//! ### Worked example
16//!
17//! Take the unit-sphere tetrahedron with **positive** `orient_3d`:
18//! `a = (0,1,0)`, `b = (1,0,0)`, `c = (0,0,1)`, `d = (-1,0,0)` —
19//! `orient_3d(a,b,c,d) = +2` (Positive). Then:
20//! - `e = (0,0,0)` (sphere centre, inside)  → `insphere = Negative`
21//! - `e = (2,0,0)` (outside)                → `insphere = Positive`
22//! - `e = (0,-1,0)` (on the sphere)         → `insphere = Zero`
23//!
24//! ### Cross-reference
25//!
26//! The orientation is determined by [`super::orient3d::orient_3d`], which
27//! returns the sign of the scalar triple product `(b-a)·((c-a)×(d-a))`
28//! (`Positive` = `d` below the oriented plane `a→b→c`, right-hand rule).
29//! Consumers (`delaunay_3`, `alpha_shape_3d`, `verify_delaunay_3`) all treat
30//! `insphere == Negative` as "inside" for a positively-oriented tet, and
31//! derive the inside-sign from the orientation sign — see those modules.
32//!
33//! ### Why not flip to the standard convention?
34//!
35//! The impl is verified against exact arithmetic (the BigInt cross-check in
36//! the test suite below) and every consumer is consistent with it. Flipping
37//! would require inverting the sign, every call-site comparison, and every
38//! test expectation in one atomic commit — a wide change in the most
39//! correctness-critical code, where a wrong sign is invalid topology. The
40//! prose was the only defect; the code + tests are the contract.
41//!
42//! The determinant (after translating by `e`) is:
43//!
44//! ```text
45//! | adx  ady  adz  adx²+ady²+adz² |
46//! | bdx  bdy  bdz  bdx²+bdy²+bdz² |
47//! | cdx  cdy  cdz  cdx²+cdy²+cdz² |
48//! | ddx  ddy  ddz  ddx²+ddy²+ddz² |
49//! ```
50//!
51//! Expanded by cofactors along the 4th column:
52//!
53//! ```text
54//! det = −ad2·M_a + bd2·M_b − cd2·M_c + dd2·M_d
55//! ```
56//!
57//! where each `M_i` is a 3×3 minor (a sum of 6 triple products of coordinate
58//! differences — structurally identical to the orient3d determinant).
59//!
60//! ## Filtered → compensated → exact ladder (Shewchuk adaptive precision)
61//!
62//! 1. **Filtered** — the 4×4 in-sphere determinant plus a static error bound.
63//! 2. **Compensated** — `mul_add` residual recovery on each product, forming a
64//!    compensated determinant with a tighter bound.
65//! 3. **Exact** — expansion arithmetic over a stack-allocated workspace sized
66//!    by [`super::expansion::MAX_EXPANSION_INSPHERE`] (2048 f64s = 16 KB).
67//!    Zero-heap, always correct, used only near degeneracy.
68//!
69//! ## Zero-heap contract
70//!
71//! No `Vec`, `String`, or `Box` in any path. The exact stage uses fixed-size
72//! stack arrays. The 16 KB stack frame is within platform limits (predicates
73//! are non-recursive).
74
75use super::expansion::{
76    compress_expansion, expansion_sum, negate_expansion, scale_expansion, sign_of_expansion,
77    two_product, Sign, MAX_EXPANSION_INSPHERE,
78};
79use super::primitives::Point3;
80
81// ──────────────────────────────────────────────────────────────────────────
82//  Error bounds
83// ──────────────────────────────────────────────────────────────────────────
84
85/// Filtered error bound for the 4×4 in-sphere determinant. The determinant
86/// has 24 terms (4 cofactors × 6 minor terms), each a product of 5 factors
87/// (3 coord diffs × 2 from the squared distance). The rounding is heavy.
88const FILTERED_BOUND: f64 = 256.0 * f64::EPSILON;
89
90/// Compensated error bound. Product residuals are recovered via `mul_add`.
91const COMPENSATED_BOUND: f64 = 64.0 * f64::EPSILON;
92
93// ──────────────────────────────────────────────────────────────────────────
94//  Coordinate differences and squared distances
95// ──────────────────────────────────────────────────────────────────────────
96
97/// All 12 coordinate differences and 4 squared distances for the in-sphere
98/// determinant (after translating by `e`).
99#[derive(Clone, Copy)]
100struct InsphereDiffs {
101    adx: f64,
102    ady: f64,
103    adz: f64,
104    bdx: f64,
105    bdy: f64,
106    bdz: f64,
107    cdx: f64,
108    cdy: f64,
109    cdz: f64,
110    ddx: f64,
111    ddy: f64,
112    ddz: f64,
113    ad2: f64,
114    bd2: f64,
115    cd2: f64,
116    dd2: f64,
117}
118
119impl InsphereDiffs {
120    #[inline]
121    fn from_points(a: Point3, b: Point3, c: Point3, d: Point3, e: Point3) -> Self {
122        let adx = a.x - e.x;
123        let ady = a.y - e.y;
124        let adz = a.z - e.z;
125        let bdx = b.x - e.x;
126        let bdy = b.y - e.y;
127        let bdz = b.z - e.z;
128        let cdx = c.x - e.x;
129        let cdy = c.y - e.y;
130        let cdz = c.z - e.z;
131        let ddx = d.x - e.x;
132        let ddy = d.y - e.y;
133        let ddz = d.z - e.z;
134        InsphereDiffs {
135            adx,
136            ady,
137            adz,
138            bdx,
139            bdy,
140            bdz,
141            cdx,
142            cdy,
143            cdz,
144            ddx,
145            ddy,
146            ddz,
147            ad2: adx * adx + ady * ady + adz * adz,
148            bd2: bdx * bdx + bdy * bdy + bdz * bdz,
149            cd2: cdx * cdx + cdy * cdy + cdz * cdz,
150            dd2: ddx * ddx + ddy * ddy + ddz * ddz,
151        }
152    }
153
154    /// The permanent: sum of absolute values of all 24 determinant terms.
155    #[inline]
156    fn permanent(&self) -> f64 {
157        let InsphereDiffs {
158            adx,
159            ady,
160            adz,
161            bdx,
162            bdy,
163            bdz,
164            cdx,
165            cdy,
166            cdz,
167            ddx,
168            ddy,
169            ddz,
170            ad2,
171            bd2,
172            cd2,
173            dd2,
174        } = *self;
175
176        // Minor_a: det(b, c, d) — 6 terms
177        let perm_a = (bdx.abs() * (cdy.abs() * ddz.abs() + cdz.abs() * ddy.abs())
178            + bdy.abs() * (cdx.abs() * ddz.abs() + cdz.abs() * ddx.abs())
179            + bdz.abs() * (cdx.abs() * ddy.abs() + cdy.abs() * ddx.abs()))
180            * ad2.abs();
181
182        // Minor_b: det(a, c, d) — 6 terms
183        let perm_b = (adx.abs() * (cdy.abs() * ddz.abs() + cdz.abs() * ddy.abs())
184            + ady.abs() * (cdx.abs() * ddz.abs() + cdz.abs() * ddx.abs())
185            + adz.abs() * (cdx.abs() * ddy.abs() + cdy.abs() * ddx.abs()))
186            * bd2.abs();
187
188        // Minor_c: det(a, b, d) — 6 terms
189        let perm_c = (adx.abs() * (bdy.abs() * ddz.abs() + bdz.abs() * ddy.abs())
190            + ady.abs() * (bdx.abs() * ddz.abs() + bdz.abs() * ddx.abs())
191            + adz.abs() * (bdx.abs() * ddy.abs() + bdy.abs() * ddx.abs()))
192            * cd2.abs();
193
194        // Minor_d: det(a, b, c) — 6 terms
195        let perm_d = (adx.abs() * (bdy.abs() * cdz.abs() + bdz.abs() * cdy.abs())
196            + ady.abs() * (bdx.abs() * cdz.abs() + bdz.abs() * cdx.abs())
197            + adz.abs() * (bdx.abs() * cdy.abs() + bdy.abs() * cdx.abs()))
198            * dd2.abs();
199
200        perm_a + perm_b + perm_c + perm_d
201    }
202}
203
204// ──────────────────────────────────────────────────────────────────────────
205//  3×3 minors (filtered)
206// ──────────────────────────────────────────────────────────────────────────
207
208/// 3×3 determinant: m1x*(m2y*m3z - m2z*m3y) - m1y*(m2x*m3z - m2z*m3x) + m1z*(m2x*m3y - m2y*m3x)
209#[inline]
210fn det3(
211    m1x: f64,
212    m1y: f64,
213    m1z: f64,
214    m2x: f64,
215    m2y: f64,
216    m2z: f64,
217    m3x: f64,
218    m3y: f64,
219    m3z: f64,
220) -> f64 {
221    m1x * (m2y * m3z - m2z * m3y) - m1y * (m2x * m3z - m2z * m3x) + m1z * (m2x * m3y - m2y * m3x)
222}
223
224// ──────────────────────────────────────────────────────────────────────────
225//  Stage 1: Filtered
226// ──────────────────────────────────────────────────────────────────────────
227
228#[inline]
229fn filtered_det(d: &InsphereDiffs) -> f64 {
230    let InsphereDiffs {
231        adx,
232        ady,
233        adz,
234        bdx,
235        bdy,
236        bdz,
237        cdx,
238        cdy,
239        cdz,
240        ddx,
241        ddy,
242        ddz,
243        ad2,
244        bd2,
245        cd2,
246        dd2,
247    } = *d;
248
249    let minor_a = det3(bdx, bdy, bdz, cdx, cdy, cdz, ddx, ddy, ddz);
250    let minor_b = det3(adx, ady, adz, cdx, cdy, cdz, ddx, ddy, ddz);
251    let minor_c = det3(adx, ady, adz, bdx, bdy, bdz, ddx, ddy, ddz);
252    let minor_d = det3(adx, ady, adz, bdx, bdy, bdz, cdx, cdy, cdz);
253
254    // Cofactor expansion along column 4 (index 3): signs are - + - +.
255    -ad2 * minor_a + bd2 * minor_b - cd2 * minor_c + dd2 * minor_d
256}
257
258// ──────────────────────────────────────────────────────────────────────────
259//  Stage 2: Compensated
260// ──────────────────────────────────────────────────────────────────────────
261
262/// Compensated 3×3 determinant with residual recovery.
263#[inline]
264fn compensated_det3(
265    m1x: f64,
266    m1y: f64,
267    m1z: f64,
268    m2x: f64,
269    m2y: f64,
270    m2z: f64,
271    m3x: f64,
272    m3y: f64,
273    m3z: f64,
274) -> f64 {
275    // Inner 2×2 minors with residual recovery.
276    let (p_yn, e_yn) = two_product(m2y, m3z);
277    let (p_zy, e_zy) = two_product(m2z, m3y);
278    let c1 = p_yn - p_zy;
279    let c1_err = e_yn - e_zy;
280
281    let (p_xn, e_xn) = two_product(m2x, m3z);
282    let (p_zx, e_zx) = two_product(m2z, m3x);
283    let c2 = p_xn - p_zx;
284    let c2_err = e_xn - e_zx;
285
286    let (p_xy, e_xy) = two_product(m2x, m3y);
287    let (p_yx, e_yx) = two_product(m2y, m3x);
288    let c3 = p_xy - p_yx;
289    let c3_err = e_xy - e_yx;
290
291    // Outer products with residual recovery.
292    let o1 = m1x * c1;
293    let o1_err = m1x.mul_add(c1, -o1) + m1x * c1_err;
294    let o2 = m1y * c2;
295    let o2_err = m1y.mul_add(c2, -o2) + m1y * c2_err;
296    let o3 = m1z * c3;
297    let o3_err = m1z.mul_add(c3, -o3) + m1z * c3_err;
298
299    (o1 - o2 + o3) + (o1_err - o2_err + o3_err)
300}
301
302#[inline]
303fn compensated_det(d: &InsphereDiffs) -> f64 {
304    let InsphereDiffs {
305        adx,
306        ady,
307        adz,
308        bdx,
309        bdy,
310        bdz,
311        cdx,
312        cdy,
313        cdz,
314        ddx,
315        ddy,
316        ddz,
317        ad2,
318        bd2,
319        cd2,
320        dd2,
321    } = *d;
322
323    let minor_a = compensated_det3(bdx, bdy, bdz, cdx, cdy, cdz, ddx, ddy, ddz);
324    let minor_b = compensated_det3(adx, ady, adz, cdx, cdy, cdz, ddx, ddy, ddz);
325    let minor_c = compensated_det3(adx, ady, adz, bdx, bdy, bdz, ddx, ddy, ddz);
326    let minor_d = compensated_det3(adx, ady, adz, bdx, bdy, bdz, cdx, cdy, cdz);
327
328    // Cofactor expansion along column 4 (index 3): signs are - + - +.
329    -ad2 * minor_a + bd2 * minor_b - cd2 * minor_c + dd2 * minor_d
330}
331
332// ──────────────────────────────────────────────────────────────────────────
333//  Stage 3: Exact (expansion arithmetic)
334// ──────────────────────────────────────────────────────────────────────────
335
336/// Compute a squared distance `dx² + dy² + dz²` as a compressed expansion.
337/// Output length ≤ 6. Writes into `out` (must be length ≥ 8) and returns the
338/// compressed length.
339fn sq_dist_3d_expansion(dx: f64, dy: f64, dz: f64, out: &mut [f64; 8]) -> usize {
340    let (px, ex) = two_product(dx, dx);
341    let (py, ey) = two_product(dy, dy);
342    let (pz, ez) = two_product(dz, dz);
343
344    // Sum three length-2 expansions → length ≤ 6.
345    let mut temp = [0.0f64; 8];
346    let len1 = expansion_sum(&[px, ex], &[py, ey], &mut temp).expect("temp sized for 4");
347    let len2 = expansion_sum(&temp[..len1], &[pz, ez], out).expect("out sized for 8");
348
349    // Compress.
350    let mut comp = [0.0f64; 8];
351    let comp_len = compress_expansion(&out[..len2], &mut comp).expect("comp sized for 8");
352    out[..comp_len].copy_from_slice(&comp[..comp_len]);
353    comp_len
354}
355
356/// Compute a 3×3 determinant as a compressed expansion.
357/// Each of the 6 terms is a product of 3 coordinate differences (length ≤ 4
358/// after two scale_expansions). Summed with compression.
359/// Writes into `accum` and returns the compressed length.
360fn exact_det3(
361    m1x: f64,
362    m1y: f64,
363    m1z: f64,
364    m2x: f64,
365    m2y: f64,
366    m2z: f64,
367    m3x: f64,
368    m3y: f64,
369    m3z: f64,
370    accum: &mut [f64; 48],
371) -> usize {
372    // 6 terms: (d1, d2, d3, negate)
373    // det3 = m1x*m2y*m3z - m1x*m2z*m3y - m1y*m2x*m3z + m1y*m2z*m3x + m1z*m2x*m3y - m1z*m2y*m3x
374    let terms: [(f64, f64, f64, bool); 6] = [
375        (m1x, m2y, m3z, false),
376        (m1x, m2z, m3y, true),
377        (m1y, m2x, m3z, true),
378        (m1y, m2z, m3x, false),
379        (m1z, m2x, m3y, false),
380        (m1z, m2y, m3x, true),
381    ];
382
383    let mut prod = [0.0f64; 2];
384    let mut term = [0.0f64; 8];
385    let mut temp = [0.0f64; 48];
386    let mut accum_len = 0usize;
387
388    for &(d1, d2, d3, negate) in &terms {
389        let (p, e) = two_product(d1, d2);
390        prod[0] = p;
391        prod[1] = e;
392        let len = scale_expansion(&prod, d3, &mut term).expect("term sized for 4");
393        // Compress the term.
394        let mut comp = [0.0f64; 8];
395        let comp_len = compress_expansion(&term[..len], &mut comp).expect("comp sized for 8");
396
397        if negate {
398            let mut neg = [0.0f64; 8];
399            neg[..comp_len].copy_from_slice(&comp[..comp_len]);
400            negate_expansion(&mut neg[..comp_len]);
401            if accum_len == 0 {
402                accum[..comp_len].copy_from_slice(&neg[..comp_len]);
403                accum_len = comp_len;
404            } else {
405                let sum_len = expansion_sum(&accum[..accum_len], &neg[..comp_len], &mut temp)
406                    .expect("temp sized for 48");
407                accum_len =
408                    compress_expansion(&temp[..sum_len], accum).expect("accum sized for 48");
409            }
410        } else {
411            if accum_len == 0 {
412                accum[..comp_len].copy_from_slice(&comp[..comp_len]);
413                accum_len = comp_len;
414            } else {
415                let sum_len = expansion_sum(&accum[..accum_len], &comp[..comp_len], &mut temp)
416                    .expect("temp sized for 48");
417                accum_len =
418                    compress_expansion(&temp[..sum_len], accum).expect("accum sized for 48");
419            }
420        }
421    }
422
423    accum_len
424}
425
426/// Multiply two expansions: `result = e * f`.
427/// Uses `scale_expansion(f, e[i])` for each component of `e`, summed with
428/// compression. Writes into `accum` (must be large enough) and returns the
429/// length. `scratch` is used for intermediate results.
430fn multiply_expansions(
431    e: &[f64],
432    f: &[f64],
433    accum: &mut [f64],
434    scratch: &mut [f64],
435    scaled: &mut [f64; 32],
436) -> usize {
437    let mut accum_len = 0usize;
438    let mut comp = [0.0f64; 32];
439
440    for &ei in e {
441        let scaled_len = scale_expansion(f, ei, scaled).expect("scaled sized for 2*f");
442        let comp_len =
443            compress_expansion(&scaled[..scaled_len], &mut comp).expect("comp sized for 32");
444
445        if accum_len == 0 {
446            accum[..comp_len].copy_from_slice(&comp[..comp_len]);
447            accum_len = comp_len;
448        } else {
449            let sum_len = expansion_sum(&accum[..accum_len], &comp[..comp_len], scratch)
450                .expect("scratch sized for accum+comp");
451            accum_len =
452                compress_expansion(&scratch[..sum_len], accum).expect("accum sized for result");
453        }
454    }
455    accum_len
456}
457
458/// The exact in-sphere determinant via expansion arithmetic. Zero-heap.
459fn exact_det(d: &InsphereDiffs) -> Sign {
460    let InsphereDiffs {
461        adx,
462        ady,
463        adz,
464        bdx,
465        bdy,
466        bdz,
467        cdx,
468        cdy,
469        cdz,
470        ddx,
471        ddy,
472        ddz,
473        ..
474    } = *d;
475
476    // Compute the four squared distances as compressed expansions.
477    let mut ad2_exp = [0.0f64; 8];
478    let mut bd2_exp = [0.0f64; 8];
479    let mut cd2_exp = [0.0f64; 8];
480    let mut dd2_exp = [0.0f64; 8];
481    let ad2_len = sq_dist_3d_expansion(adx, ady, adz, &mut ad2_exp);
482    let bd2_len = sq_dist_3d_expansion(bdx, bdy, bdz, &mut bd2_exp);
483    let cd2_len = sq_dist_3d_expansion(cdx, cdy, cdz, &mut cd2_exp);
484    let dd2_len = sq_dist_3d_expansion(ddx, ddy, ddz, &mut dd2_exp);
485
486    // Compute the four 3×3 minors as compressed expansions.
487    let mut minor_a = [0.0f64; 48];
488    let mut minor_b = [0.0f64; 48];
489    let mut minor_c = [0.0f64; 48];
490    let mut minor_d = [0.0f64; 48];
491    let ma_len = exact_det3(bdx, bdy, bdz, cdx, cdy, cdz, ddx, ddy, ddz, &mut minor_a);
492    let mb_len = exact_det3(adx, ady, adz, cdx, cdy, cdz, ddx, ddy, ddz, &mut minor_b);
493    let mc_len = exact_det3(adx, ady, adz, bdx, bdy, bdz, ddx, ddy, ddz, &mut minor_c);
494    let md_len = exact_det3(adx, ady, adz, bdx, bdy, bdz, cdx, cdy, cdz, &mut minor_d);
495
496    // det = -ad2*minor_a + bd2*minor_b - cd2*minor_c + dd2*minor_d
497    // Cofactor expansion along column 4 (index 3): signs are - + - +.
498    let mut accum = [0.0f64; MAX_EXPANSION_INSPHERE];
499    let mut temp = [0.0f64; MAX_EXPANSION_INSPHERE];
500    let mut product = [0.0f64; 256]; // minor (≤48) × sq_dist (≤8) → ≤ 48*16 = 768
501    let mut scratch = [0.0f64; MAX_EXPANSION_INSPHERE];
502    let mut scaled = [0.0f64; 32]; // for multiply_expansions
503    let mut accum_len = 0usize;
504
505    // Helper closure: multiply sq_dist × minor, then add (or subtract) to accum.
506    macro_rules! add_product {
507        ($sq:expr, $sq_len:expr, $min:expr, $min_len:expr, $negate:expr) => {
508            let prod_len = multiply_expansions(
509                &$sq[..$sq_len],
510                &$min[..$min_len],
511                &mut product,
512                &mut scratch,
513                &mut scaled,
514            );
515            if $negate {
516                negate_expansion(&mut product[..prod_len]);
517            }
518            if accum_len == 0 {
519                accum[..prod_len].copy_from_slice(&product[..prod_len]);
520                accum_len = prod_len;
521            } else {
522                let sum_len = expansion_sum(&accum[..accum_len], &product[..prod_len], &mut temp)
523                    .expect("temp sized for MAX_EXPANSION_INSPHERE");
524                accum_len = compress_expansion(&temp[..sum_len], &mut accum)
525                    .expect("accum sized for MAX_EXPANSION_INSPHERE");
526            }
527        };
528    }
529
530    add_product!(ad2_exp, ad2_len, minor_a, ma_len, true); // -ad2*minor_a
531    add_product!(bd2_exp, bd2_len, minor_b, mb_len, false); // +bd2*minor_b
532    add_product!(cd2_exp, cd2_len, minor_c, mc_len, true); // -cd2*minor_c
533    add_product!(dd2_exp, dd2_len, minor_d, md_len, false); // +dd2*minor_d
534
535    // Final compress and sign.
536    let mut compressed = [0.0f64; MAX_EXPANSION_INSPHERE];
537    let comp_len = compress_expansion(&accum[..accum_len], &mut compressed)
538        .expect("compressed sized for MAX_EXPANSION_INSPHERE");
539    sign_of_expansion(&compressed[..comp_len])
540}
541
542// ──────────────────────────────────────────────────────────────────────────
543//  Public ladder entry point
544// ──────────────────────────────────────────────────────────────────────────
545
546/// The 3-D in-sphere predicate: side of `e` w.r.t. the oriented sphere
547/// through `a, b, c, d`.
548///
549/// **Sign convention (non-standard — see module docs):** when `a, b, c, d`
550/// are positively oriented ([`super::orient3d::orient_3d`] > 0), returns
551/// [`Sign::Negative`] if `e` is **inside** the sphere, [`Sign::Zero`] if `e`
552/// is on it, [`Sign::Positive`] if **outside**. The sense is reversed for
553/// negative orientation (inside = `Positive`).
554///
555/// This is the public ladder entry point — it escalates from filtered to
556/// compensated to exact as needed, never returning an uncertain sign.
557pub fn insphere(a: Point3, b: Point3, c: Point3, d: Point3, e: Point3) -> Sign {
558    let diffs = InsphereDiffs::from_points(a, b, c, d, e);
559    let perm = diffs.permanent();
560
561    let det = filtered_det(&diffs);
562    if det.abs() > perm * FILTERED_BOUND {
563        return Sign::from_f64(det);
564    }
565
566    let comp = compensated_det(&diffs);
567    if comp.abs() > perm * COMPENSATED_BOUND {
568        return Sign::from_f64(comp);
569    }
570
571    exact_det(&diffs)
572}
573
574// ──────────────────────────────────────────────────────────────────────────
575//  Tests
576// ──────────────────────────────────────────────────────────────────────────
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581    use crate::specialized_libs::computational_geometry::exact_test_helper::Exact;
582
583    /// Ground-truth in-sphere sign via BigInt.
584    /// Computes coordinate differences in f64 first (matching the predicate),
585    /// then converts to exact BigInt values.
586    fn exact_insphere_sign(a: Point3, b: Point3, c: Point3, d: Point3, e: Point3) -> Sign {
587        let adx_f = a.x - e.x;
588        let ady_f = a.y - e.y;
589        let adz_f = a.z - e.z;
590        let bdx_f = b.x - e.x;
591        let bdy_f = b.y - e.y;
592        let bdz_f = b.z - e.z;
593        let cdx_f = c.x - e.x;
594        let cdy_f = c.y - e.y;
595        let cdz_f = c.z - e.z;
596        let ddx_f = d.x - e.x;
597        let ddy_f = d.y - e.y;
598        let ddz_f = d.z - e.z;
599
600        let adx = Exact::from_f64(adx_f);
601        let ady = Exact::from_f64(ady_f);
602        let adz = Exact::from_f64(adz_f);
603        let bdx = Exact::from_f64(bdx_f);
604        let bdy = Exact::from_f64(bdy_f);
605        let bdz = Exact::from_f64(bdz_f);
606        let cdx = Exact::from_f64(cdx_f);
607        let cdy = Exact::from_f64(cdy_f);
608        let cdz = Exact::from_f64(cdz_f);
609        let ddx = Exact::from_f64(ddx_f);
610        let ddy = Exact::from_f64(ddy_f);
611        let ddz = Exact::from_f64(ddz_f);
612
613        let ad2 = adx
614            .clone()
615            .mul(adx.clone())
616            .add(ady.clone().mul(ady.clone()))
617            .add(adz.clone().mul(adz.clone()));
618        let bd2 = bdx
619            .clone()
620            .mul(bdx.clone())
621            .add(bdy.clone().mul(bdy.clone()))
622            .add(bdz.clone().mul(bdz.clone()));
623        let cd2 = cdx
624            .clone()
625            .mul(cdx.clone())
626            .add(cdy.clone().mul(cdy.clone()))
627            .add(cdz.clone().mul(cdz.clone()));
628        let dd2 = ddx
629            .clone()
630            .mul(ddx.clone())
631            .add(ddy.clone().mul(ddy.clone()))
632            .add(ddz.clone().mul(ddz.clone()));
633
634        // 3×3 minors via BigInt
635        let minor_a = {
636            let t1 = bdx.clone().mul(cdy.clone()).mul(ddz.clone());
637            let t2 = bdx.clone().mul(cdz.clone()).mul(ddy.clone());
638            let t3 = bdy.clone().mul(cdx.clone()).mul(ddz.clone());
639            let t4 = bdy.clone().mul(cdz.clone()).mul(ddx.clone());
640            let t5 = bdz.clone().mul(cdx.clone()).mul(ddy.clone());
641            let t6 = bdz.clone().mul(cdy.clone()).mul(ddx.clone());
642            t1.sub(t2).sub(t3).add(t4).add(t5).sub(t6)
643        };
644        let minor_b = {
645            let t1 = adx.clone().mul(cdy.clone()).mul(ddz.clone());
646            let t2 = adx.clone().mul(cdz.clone()).mul(ddy.clone());
647            let t3 = ady.clone().mul(cdx.clone()).mul(ddz.clone());
648            let t4 = ady.clone().mul(cdz.clone()).mul(ddx.clone());
649            let t5 = adz.clone().mul(cdx.clone()).mul(ddy.clone());
650            let t6 = adz.clone().mul(cdy.clone()).mul(ddx.clone());
651            t1.sub(t2).sub(t3).add(t4).add(t5).sub(t6)
652        };
653        let minor_c = {
654            let t1 = adx.clone().mul(bdy.clone()).mul(ddz.clone());
655            let t2 = adx.clone().mul(bdz.clone()).mul(ddy.clone());
656            let t3 = ady.clone().mul(bdx.clone()).mul(ddz.clone());
657            let t4 = ady.clone().mul(bdz.clone()).mul(ddx.clone());
658            let t5 = adz.clone().mul(bdx.clone()).mul(ddy.clone());
659            let t6 = adz.clone().mul(bdy.clone()).mul(ddx.clone());
660            t1.sub(t2).sub(t3).add(t4).add(t5).sub(t6)
661        };
662        let minor_d = {
663            let t1 = adx.clone().mul(bdy.clone()).mul(cdz.clone());
664            let t2 = adx.clone().mul(bdz.clone()).mul(cdy.clone());
665            let t3 = ady.clone().mul(bdx.clone()).mul(cdz.clone());
666            let t4 = ady.clone().mul(bdz.clone()).mul(cdx.clone());
667            let t5 = adz.clone().mul(bdx.clone()).mul(cdy.clone());
668            let t6 = adz.clone().mul(bdy.clone()).mul(cdx.clone());
669            t1.sub(t2).sub(t3).add(t4).add(t5).sub(t6)
670        };
671
672        // det = -ad2*minor_a + bd2*minor_b - cd2*minor_c + dd2*minor_d
673        let det = ad2
674            .mul(minor_a)
675            .neg()
676            .add(bd2.mul(minor_b))
677            .sub(cd2.mul(minor_c))
678            .add(dd2.mul(minor_d));
679        det.sign()
680    }
681
682    // ── Basic classification ──────────────────────────────────────────────
683
684    /// Unit sphere centered at origin. a,b,c,d on sphere with **negative**
685    /// orientation (`orient_3d = -2`). Under this impl's convention,
686    /// negative-orientation + inside ⇒ `Positive`.
687    fn unit_sphere_points() -> (Point3, Point3, Point3, Point3) {
688        let a = Point3::new(1.0, 0.0, 0.0);
689        let b = Point3::new(0.0, 1.0, 0.0);
690        let c = Point3::new(0.0, 0.0, 1.0);
691        let d = Point3::new(-1.0, 0.0, 0.0);
692        (a, b, c, d)
693    }
694
695    #[test]
696    fn classifies_inside_sphere() {
697        let (a, b, c, d) = unit_sphere_points();
698        let e = Point3::new(0.0, 0.0, 0.0); // center → inside
699                                            // Negative orientation ⇒ inside = Positive (this impl's convention).
700        assert_eq!(insphere(a, b, c, d, e), Sign::Positive);
701    }
702
703    #[test]
704    fn classifies_outside_sphere() {
705        let (a, b, c, d) = unit_sphere_points();
706        let e = Point3::new(2.0, 0.0, 0.0); // outside
707                                            // Negative orientation ⇒ outside = Negative.
708        assert_eq!(insphere(a, b, c, d, e), Sign::Negative);
709    }
710
711    #[test]
712    fn classifies_on_sphere() {
713        let (a, b, c, d) = unit_sphere_points();
714        let e = Point3::new(0.0, -1.0, 0.0); // on the unit sphere
715        assert_eq!(insphere(a, b, c, d, e), Sign::Zero);
716    }
717
718    #[test]
719    fn sign_flips_for_negative_orientation() {
720        // Swap a and b to flip orientation: this tet has **positive**
721        // orientation (`orient_3d = +2`). Under this impl's convention,
722        // positive-orientation + inside ⇒ `Negative` (the flip).
723        let a = Point3::new(0.0, 1.0, 0.0);
724        let b = Point3::new(1.0, 0.0, 0.0);
725        let c = Point3::new(0.0, 0.0, 1.0);
726        let d = Point3::new(-1.0, 0.0, 0.0);
727        let e = Point3::new(0.0, 0.0, 0.0); // center → inside, but orientation is now positive
728        let s = insphere(a, b, c, d, e);
729        // With positive orientation, inside → Negative (this impl's convention).
730        assert_eq!(s, Sign::Negative);
731    }
732
733    // ── Agreement with BigInt cross-check ─────────────────────────────────
734
735    #[test]
736    fn agrees_with_exact_on_basic_cases() {
737        let (a, b, c, d) = unit_sphere_points();
738        let cases = [
739            (a, b, c, d, Point3::new(0.0, 0.0, 0.0)),  // inside
740            (a, b, c, d, Point3::new(2.0, 0.0, 0.0)),  // outside
741            (a, b, c, d, Point3::new(0.0, -1.0, 0.0)), // on sphere
742            (a, b, c, d, Point3::new(0.5, 0.5, 0.5)),  // inside
743            (
744                Point3::new(3.0, 4.0, 0.0),
745                Point3::new(0.0, 0.0, 0.0),
746                Point3::new(6.0, 0.0, 0.0),
747                Point3::new(3.0, 0.0, 4.0),
748                Point3::new(3.0, 1.0, 0.0),
749            ),
750        ];
751        for (a, b, c, d, e) in cases {
752            assert_eq!(
753                insphere(a, b, c, d, e),
754                exact_insphere_sign(a, b, c, d, e),
755                "mismatch on ({a:?}, {b:?}, {c:?}, {d:?}, {e:?})"
756            );
757        }
758    }
759
760    // ── Adversarial: cospherical (exact zero) ─────────────────────────────
761
762    #[test]
763    fn cospherical_five_points() {
764        // Five points on the unit sphere
765        let a = Point3::new(1.0, 0.0, 0.0);
766        let b = Point3::new(0.0, 1.0, 0.0);
767        let c = Point3::new(0.0, 0.0, 1.0);
768        let d = Point3::new(-1.0, 0.0, 0.0);
769        let e = Point3::new(0.0, -1.0, 0.0);
770        assert_eq!(insphere(a, b, c, d, e), Sign::Zero);
771        assert_eq!(exact_insphere_sign(a, b, c, d, e), Sign::Zero);
772    }
773
774    #[test]
775    fn cospherical_on_arbitrary_sphere() {
776        // Sphere centered at (1, 2, 3), radius 6.
777        // Points: (7,2,3), (1,8,3), (1,2,9), (-5,2,3), (1,-4,3)
778        let a = Point3::new(7.0, 2.0, 3.0);
779        let b = Point3::new(1.0, 8.0, 3.0);
780        let c = Point3::new(1.0, 2.0, 9.0);
781        let d = Point3::new(-5.0, 2.0, 3.0);
782        let e = Point3::new(1.0, -4.0, 3.0);
783        assert_eq!(insphere(a, b, c, d, e), Sign::Zero);
784        assert_eq!(exact_insphere_sign(a, b, c, d, e), Sign::Zero);
785    }
786
787    // ── Adversarial: near-cospherical (±1-ulp) ────────────────────────────
788
789    #[test]
790    fn near_cospherical_1ulp_off() {
791        let a = Point3::new(1.0, 0.0, 0.0);
792        let b = Point3::new(0.0, 1.0, 0.0);
793        let c = Point3::new(0.0, 0.0, 1.0);
794        let d = Point3::new(-1.0, 0.0, 0.0);
795        let e0 = Point3::new(0.0, -1.0, 0.0); // on sphere
796
797        for &delta_bits in &[1i64, -1, 2, -2, 5, -5, 100, -100] {
798            let ey = f64::from_bits((e0.y.to_bits() as i64 + delta_bits) as u64);
799            let e = Point3::new(e0.x, ey, e0.z);
800            assert_eq!(
801                insphere(a, b, c, d, e),
802                exact_insphere_sign(a, b, c, d, e),
803                "mismatch on near-cospherical delta_bits={delta_bits}"
804            );
805        }
806    }
807
808    // ── Adversarial: extreme exponents ────────────────────────────────────
809
810    #[test]
811    fn extreme_exponents_agree_with_exact() {
812        // Keep products within f64 range: coords ~1e30, sq_dist ~1e60, products ~1e150.
813        let cases = [
814            (
815                Point3::new(1e30, 0.0, 0.0),
816                Point3::new(0.0, 1e30, 0.0),
817                Point3::new(0.0, 0.0, 1e30),
818                Point3::new(-1e30, 0.0, 0.0),
819                Point3::new(0.0, 0.0, 0.0),
820            ),
821            (
822                Point3::new(1e-30, 0.0, 0.0),
823                Point3::new(0.0, 1e-30, 0.0),
824                Point3::new(0.0, 0.0, 1e-30),
825                Point3::new(-1e-30, 0.0, 0.0),
826                Point3::new(0.0, 0.0, 0.0),
827            ),
828        ];
829        for (a, b, c, d, e) in cases {
830            assert_eq!(
831                insphere(a, b, c, d, e),
832                exact_insphere_sign(a, b, c, d, e),
833                "mismatch on extreme-exponent case"
834            );
835        }
836    }
837
838    // ── Adversarial: cancellation ─────────────────────────────────────────
839
840    #[test]
841    fn cancellation_agrees_with_exact() {
842        let a = Point3::new(1e30, 0.0, 0.0);
843        let b = Point3::new(0.0, 1e30, 0.0);
844        let c = Point3::new(0.0, 0.0, 1e30);
845        let d = Point3::new(-1e30, 0.0, 0.0);
846        let e = Point3::new(0.0, -1e30, 0.0); // cospherical
847        assert_eq!(insphere(a, b, c, d, e), exact_insphere_sign(a, b, c, d, e));
848    }
849
850    #[test]
851    fn massive_cancellation_agrees_with_exact() {
852        let a = Point3::new(1e30, 0.0, 0.0);
853        let b = Point3::new(0.0, 1e30, 0.0);
854        let c = Point3::new(0.0, 0.0, 1e30);
855        let d = Point3::new(-1e30, 0.0, 0.0);
856        let e = Point3::new(0.0, -1e30 + 1.0, 0.0); // just off the sphere
857        assert_eq!(
858            insphere(a, b, c, d, e),
859            exact_insphere_sign(a, b, c, d, e),
860            "mismatch on massive cancellation"
861        );
862    }
863
864    // ── All three ladder stages exercised ─────────────────────────────────
865
866    #[test]
867    fn filtered_stage_resolves_clear_case() {
868        let (a, b, c, d) = unit_sphere_points();
869        let e = Point3::new(0.0, 0.0, 0.0);
870        let diffs = InsphereDiffs::from_points(a, b, c, d, e);
871        let det = filtered_det(&diffs);
872        let perm = diffs.permanent();
873        assert!(
874            det.abs() > perm * FILTERED_BOUND,
875            "filtered should resolve (det={det}, bound={})",
876            perm * FILTERED_BOUND
877        );
878        assert_eq!(insphere(a, b, c, d, e), Sign::Positive);
879    }
880
881    #[test]
882    fn near_cospherical_resolves_via_compensated_or_exact() {
883        let a = Point3::new(1.0, 0.0, 0.0);
884        let b = Point3::new(0.0, 1.0, 0.0);
885        let c = Point3::new(0.0, 0.0, 1.0);
886        let d = Point3::new(-1.0, 0.0, 0.0);
887        let e = Point3::new(0.0, -1.0 + 1e-15, 0.0);
888        assert_eq!(
889            insphere(a, b, c, d, e),
890            exact_insphere_sign(a, b, c, d, e),
891            "near-cospherical must match exact"
892        );
893    }
894
895    // ── Determinism ───────────────────────────────────────────────────────
896
897    #[test]
898    fn deterministic_across_calls() {
899        let (a, b, c, d) = unit_sphere_points();
900        let e = Point3::new(0.3, 0.3, 0.3);
901        let s1 = insphere(a, b, c, d, e);
902        let s2 = insphere(a, b, c, d, e);
903        assert_eq!(s1, s2);
904    }
905
906    // ── Symmetry ──────────────────────────────────────────────────────────
907
908    #[test]
909    fn swapping_a_b_flips_sign() {
910        let (a, b, c, d) = unit_sphere_points();
911        let e = Point3::new(0.0, 0.0, 0.0);
912        let s = insphere(a, b, c, d, e);
913        let s_swapped = insphere(b, a, c, d, e);
914        assert_eq!(s, s_swapped.flip());
915    }
916
917    #[test]
918    fn translation_invariant() {
919        let (a, b, c, d) = unit_sphere_points();
920        let e = Point3::new(0.0, 0.0, 0.0);
921        let t = Point3::new(1e10, -1e10, 5e9);
922        let s = insphere(a, b, c, d, e);
923        let s_t = insphere(
924            Point3::new(a.x + t.x, a.y + t.y, a.z + t.z),
925            Point3::new(b.x + t.x, b.y + t.y, b.z + t.z),
926            Point3::new(c.x + t.x, c.y + t.y, c.z + t.z),
927            Point3::new(d.x + t.x, d.y + t.y, d.z + t.z),
928            Point3::new(e.x + t.x, e.y + t.y, e.z + t.z),
929        );
930        assert_eq!(s, s_t);
931    }
932
933    // ── Zero-heap contract ────────────────────────────────────────────────
934
935    #[test]
936    fn no_heap_allocation_in_predicate() {
937        let (a, b, c, d) = unit_sphere_points();
938        let e = Point3::new(0.0, -1.0, 0.0); // cospherical — exercises exact stage
939        let _ = insphere(a, b, c, d, e);
940    }
941}