Skip to main content

qualia_core_db/specialized_libs/computational_geometry/
incircle.rs

1//! `incircle` — the 2-D in-circle predicate (P1.5).
2//!
3//! Computes the sign of the determinant that classifies whether a point `d`
4//! lies inside, on, or outside the oriented circle through `a, b, c`. When
5//! `a, b, c` are counter-clockwise, [`Sign::Positive`] means `d` is inside the
6//! circle, [`Sign::Zero`] means on it, [`Sign::Negative`] means outside. The
7//! sign flips when `a, b, c` are clockwise.
8//!
9//! The determinant (after translating by `d`) is:
10//!
11//! ```text
12//! | adx  ady  adx²+ady² |
13//! | bdx  bdy  bdx²+bdy² |
14//! | cdx  cdy  cdx²+cdy² |
15//! ```
16//!
17//! where `adx = ax − dx`, etc. This is a 3×3 determinant whose third column
18//! contains squared-distance entries — each term is a product of two
19//! coordinate differences and a squared distance.
20//!
21//! ## Filtered → compensated → exact ladder (Shewchuk adaptive precision)
22//!
23//! 1. **Filtered** — the 3×3 in-circle determinant (with squared-distance
24//!    entries) plus a static error bound.
25//! 2. **Compensated** — `mul_add` residual recovery on each product, forming a
26//!    compensated determinant with a tighter bound.
27//! 3. **Exact** — expansion arithmetic over a stack-allocated workspace sized
28//!    by [`super::expansion::MAX_EXPANSION_INCIRCLE`]. Zero-heap, always
29//!    correct, used only near degeneracy.
30//!
31//! ## Zero-heap contract
32//!
33//! No `Vec`, `String`, or `Box` in any path. The exact stage uses fixed-size
34//! stack arrays.
35//!
36//! ## References
37//!
38//! The adaptive-precision ladder follows Shewchuk (1996). The implementation
39//! is original Rust over the P1.3 expansion primitives.
40
41use super::expansion::{
42    compress_expansion, expansion_sum, negate_expansion, scale_expansion, sign_of_expansion,
43    two_product, Sign, MAX_EXPANSION_INCIRCLE,
44};
45use super::primitives::Point2;
46
47// ──────────────────────────────────────────────────────────────────────────
48//  Error bounds
49// ──────────────────────────────────────────────────────────────────────────
50
51/// Filtered error bound coefficient for the in-circle determinant. The
52/// determinant is a sum of 6 products, each involving 2 coordinate differences
53/// and a squared distance (itself a sum of 2 products). The rounding is
54/// heavier than orient3d, so the bound is larger.
55const FILTERED_BOUND: f64 = 32.0 * f64::EPSILON;
56
57/// Compensated error bound coefficient. Product residuals are recovered via
58/// `mul_add`, leaving only summation rounding.
59const COMPENSATED_BOUND: f64 = 8.0 * f64::EPSILON;
60
61// ──────────────────────────────────────────────────────────────────────────
62//  Coordinate differences and squared distances
63// ──────────────────────────────────────────────────────────────────────────
64
65/// The 6 coordinate differences and 3 squared distances for the in-circle
66/// determinant (after translating by `d`).
67#[derive(Clone, Copy)]
68struct IncircleDiffs {
69    adx: f64,
70    ady: f64,
71    bdx: f64,
72    bdy: f64,
73    cdx: f64,
74    cdy: f64,
75    ad2: f64,
76    bd2: f64,
77    cd2: f64,
78}
79
80impl IncircleDiffs {
81    #[inline]
82    fn from_points(a: Point2, b: Point2, c: Point2, d: Point2) -> Self {
83        let adx = a.x - d.x;
84        let ady = a.y - d.y;
85        let bdx = b.x - d.x;
86        let bdy = b.y - d.y;
87        let cdx = c.x - d.x;
88        let cdy = c.y - d.y;
89        IncircleDiffs {
90            adx,
91            ady,
92            bdx,
93            bdy,
94            cdx,
95            cdy,
96            ad2: adx * adx + ady * ady,
97            bd2: bdx * bdx + bdy * bdy,
98            cd2: cdx * cdx + cdy * cdy,
99        }
100    }
101
102    /// The permanent: sum of absolute values of the 6 determinant terms.
103    #[inline]
104    fn permanent(&self) -> f64 {
105        let IncircleDiffs {
106            adx,
107            ady,
108            bdx,
109            bdy,
110            cdx,
111            cdy,
112            ad2,
113            bd2,
114            cd2,
115        } = *self;
116        (adx.abs() * (bdy.abs() * cd2.abs() + cdy.abs() * bd2.abs()))
117            + (ady.abs() * (bdx.abs() * cd2.abs() + cdx.abs() * bd2.abs()))
118            + (ad2.abs() * (bdx.abs() * cdy.abs() + bdy.abs() * cdx.abs()))
119    }
120}
121
122// ──────────────────────────────────────────────────────────────────────────
123//  Stage 1: Filtered
124// ──────────────────────────────────────────────────────────────────────────
125
126#[inline]
127fn filtered_det(d: &IncircleDiffs) -> f64 {
128    let IncircleDiffs {
129        adx,
130        ady,
131        bdx,
132        bdy,
133        cdx,
134        cdy,
135        ad2,
136        bd2,
137        cd2,
138    } = *d;
139    adx * (bdy * cd2 - cdy * bd2) - ady * (bdx * cd2 - cdx * bd2) + ad2 * (bdx * cdy - bdy * cdx)
140}
141
142// ──────────────────────────────────────────────────────────────────────────
143//  Stage 2: Compensated
144// ──────────────────────────────────────────────────────────────────────────
145
146#[inline]
147fn compensated_det(d: &IncircleDiffs) -> f64 {
148    let IncircleDiffs {
149        adx,
150        ady,
151        bdx,
152        bdy,
153        cdx,
154        cdy,
155        ad2,
156        bd2,
157        cd2,
158    } = *d;
159
160    // Compensated squared distances: ad2 + ad2_err = adx² + ady² (exact).
161    // The filtered `ad2` is `round(round(adx²) + round(ady²))`. The error
162    // includes both the product residuals and the addition rounding.
163    let (adx2_p, adx2_e) = two_product(adx, adx);
164    let (ady2_p, ady2_e) = two_product(ady, ady);
165    let ad2_err = (adx2_p + ady2_p - ad2) + adx2_e + ady2_e;
166
167    let (bdx2_p, bdx2_e) = two_product(bdx, bdx);
168    let (bdy2_p, bdy2_e) = two_product(bdy, bdy);
169    let bd2_err = (bdx2_p + bdy2_p - bd2) + bdx2_e + bdy2_e;
170
171    let (cdx2_p, cdx2_e) = two_product(cdx, cdx);
172    let (cdy2_p, cdy2_e) = two_product(cdy, cdy);
173    let cd2_err = (cdx2_p + cdy2_p - cd2) + cdx2_e + cdy2_e;
174
175    // Inner products with residual recovery (use filtered ad2/bd2/cd2 as main).
176    let (p_bdy_cd2, e_bdy_cd2) = two_product(bdy, cd2);
177    let (p_cdy_bd2, e_cdy_bd2) = two_product(cdy, bd2);
178    let (p_bdx_cd2, e_bdx_cd2) = two_product(bdx, cd2);
179    let (p_cdx_bd2, e_cdx_bd2) = two_product(cdx, bd2);
180    let (p_bdx_cdy, e_bdx_cdy) = two_product(bdx, cdy);
181    let (p_bdy_cdx, e_bdy_cdx) = two_product(bdy, cdx);
182
183    // Three 2×2 minors.
184    let m1 = p_bdy_cd2 - p_cdy_bd2;
185    let m1_err = e_bdy_cd2 - e_cdy_bd2 + bdy * cd2_err - cdy * bd2_err;
186    let m2 = p_bdx_cd2 - p_cdx_bd2;
187    let m2_err = e_bdx_cd2 - e_cdx_bd2 + bdx * cd2_err - cdx * bd2_err;
188    let m3 = p_bdx_cdy - p_bdy_cdx;
189    let m3_err = e_bdx_cdy - e_bdy_cdx;
190
191    // Outer products — use the filtered ad2/bd2/cd2 as main values, with errors.
192    let o1 = adx * m1;
193    let o1_err = adx.mul_add(m1, -o1) + adx * m1_err;
194    let o2 = ady * m2;
195    let o2_err = ady.mul_add(m2, -o2) + ady * m2_err;
196    let o3 = ad2 * m3;
197    let o3_err = ad2.mul_add(m3, -o3) + ad2_err * m3 + ad2 * m3_err;
198
199    (o1 - o2 + o3) + (o1_err - o2_err + o3_err)
200}
201
202// ──────────────────────────────────────────────────────────────────────────
203//  Stage 3: Exact (expansion arithmetic)
204// ──────────────────────────────────────────────────────────────────────────
205
206/// The exact in-circle determinant via expansion arithmetic. Zero-heap.
207///
208/// The 6 terms each involve a squared-distance expansion (length ≤ 4) scaled
209/// by two coordinate differences. Each term is at most length 16; the 6 terms
210/// are summed with compression after each addition into the 96-element
211/// workspace.
212fn exact_det(d: &IncircleDiffs) -> Sign {
213    let IncircleDiffs {
214        adx,
215        ady,
216        bdx,
217        bdy,
218        cdx,
219        cdy,
220        ..
221    } = *d;
222
223    // Compute the three squared distances as expansions (length ≤ 4 each).
224    let mut ad2_exp = [0.0f64; 4];
225    let mut bd2_exp = [0.0f64; 4];
226    let mut cd2_exp = [0.0f64; 4];
227    let ad2_len = sq_dist_expansion(adx, ady, &mut ad2_exp);
228    let bd2_len = sq_dist_expansion(bdx, bdy, &mut bd2_exp);
229    let cd2_len = sq_dist_expansion(cdx, cdy, &mut cd2_exp);
230
231    // The 6 terms: (scalar1, scalar2, expansion, negate)
232    // det = adx*bdy*cd2 - adx*cdy*bd2 - ady*bdx*cd2 + ady*cdx*bd2 + ad2*bdx*cdy - ad2*bdy*cdx
233    let terms: [(f64, f64, &[f64], bool); 6] = [
234        (adx, bdy, &cd2_exp[..cd2_len], false),
235        (adx, cdy, &bd2_exp[..bd2_len], true),
236        (ady, bdx, &cd2_exp[..cd2_len], true),
237        (ady, cdx, &bd2_exp[..bd2_len], false),
238        (1.0, bdx, &cd2_exp[..cd2_len], false), // placeholder, replaced below
239        (1.0, bdy, &cd2_exp[..cd2_len], true),  // placeholder
240    ];
241
242    // We need ad2_exp for terms 5 and 6. Handle them separately.
243    // Term 5: +ad2 * bdx * cdy  (ad2 is the expansion, bdx and cdy are scalars)
244    // Term 6: -ad2 * bdy * cdx
245
246    // Stack workspace.
247    let mut scaled = [0.0f64; 16]; // expansion × scalar → length ≤ 2*expansion_len
248    let mut term = [0.0f64; 32]; // after second scale → length ≤ 4*expansion_len
249    let mut accum = [0.0f64; MAX_EXPANSION_INCIRCLE];
250    let mut temp = [0.0f64; MAX_EXPANSION_INCIRCLE];
251    let mut accum_len = 0usize;
252
253    // Process the first 4 terms (cd2 or bd2 as the expansion).
254    for &(s1, s2, exp, negate) in &terms[..4] {
255        let term_len = compute_term(exp, s1, s2, &mut scaled, &mut term);
256        add_term(
257            &mut accum,
258            &mut accum_len,
259            &term[..term_len],
260            negate,
261            &mut temp,
262        );
263    }
264
265    // Process terms 5 and 6 (ad2 as the expansion).
266    // Term 5: +ad2 * bdx * cdy
267    let t5_len = compute_term(&ad2_exp[..ad2_len], bdx, cdy, &mut scaled, &mut term);
268    add_term(
269        &mut accum,
270        &mut accum_len,
271        &term[..t5_len],
272        false,
273        &mut temp,
274    );
275    // Term 6: -ad2 * bdy * cdx
276    let t6_len = compute_term(&ad2_exp[..ad2_len], bdy, cdx, &mut scaled, &mut term);
277    add_term(&mut accum, &mut accum_len, &term[..t6_len], true, &mut temp);
278
279    // Final compress and sign.
280    let mut compressed = [0.0f64; MAX_EXPANSION_INCIRCLE];
281    let comp_len = compress_expansion(&accum[..accum_len], &mut compressed)
282        .expect("compressed buffer sized for MAX_EXPANSION_INCIRCLE");
283    sign_of_expansion(&compressed[..comp_len])
284}
285
286/// Compute `adx² + ady²` as an expansion (length ≤ 4).
287fn sq_dist_expansion(dx: f64, dy: f64, out: &mut [f64; 4]) -> usize {
288    let (px, ex) = two_product(dx, dx);
289    let (py, ey) = two_product(dy, dy);
290    // Sum the two length-2 expansions → length ≤ 4.
291    let prod = [px, ex];
292    let other = [py, ey];
293    let len = expansion_sum(&prod, &other, out).expect("out is sized for 4");
294    // Compress to minimal form.
295    let mut comp = [0.0f64; 4];
296    let comp_len = compress_expansion(&out[..len], &mut comp).expect("comp is sized for 4");
297    out[..comp_len].copy_from_slice(&comp[..comp_len]);
298    comp_len
299}
300
301/// Compute `exp * s1 * s2` as an expansion. Writes into `scaled` (scratch) and
302/// `term` (output). Returns the length of the result in `term`.
303///
304/// `scaled` must have length ≥ 2 * exp.len(). `term` must have length ≥
305/// 4 * exp.len().
306fn compute_term(
307    exp: &[f64],
308    s1: f64,
309    s2: f64,
310    scaled: &mut [f64; 16],
311    term: &mut [f64; 32],
312) -> usize {
313    // Scale exp by s1 → length ≤ 2*exp.len().
314    let len1 = scale_expansion(exp, s1, scaled).expect("scaled sized for 2*exp");
315    // Compress to keep it small.
316    let mut comp = [0.0f64; 16];
317    let comp_len = compress_expansion(&scaled[..len1], &mut comp).expect("comp sized for 16");
318    // Scale by s2 → length ≤ 2*comp_len.
319    let len2 = scale_expansion(&comp[..comp_len], s2, term).expect("term sized for 32");
320    // Compress the result.
321    let mut comp2 = [0.0f64; 32];
322    let comp2_len = compress_expansion(&term[..len2], &mut comp2).expect("comp2 sized for 32");
323    term[..comp2_len].copy_from_slice(&comp2[..comp2_len]);
324    comp2_len
325}
326
327/// Add a term (with optional negation) to the accumulator, then compress.
328fn add_term(
329    accum: &mut [f64],
330    accum_len: &mut usize,
331    term: &[f64],
332    negate: bool,
333    temp: &mut [f64],
334) {
335    let mut neg_term = [0.0f64; 32];
336    if negate {
337        neg_term[..term.len()].copy_from_slice(term);
338        negate_expansion(&mut neg_term[..term.len()]);
339        if *accum_len == 0 {
340            accum[..term.len()].copy_from_slice(&neg_term[..term.len()]);
341            *accum_len = term.len();
342            return;
343        }
344        let sum_len = expansion_sum(&accum[..*accum_len], &neg_term[..term.len()], temp)
345            .expect("temp sized for MAX_EXPANSION_INCIRCLE");
346        *accum_len = compress_expansion(&temp[..sum_len], accum)
347            .expect("accum sized for MAX_EXPANSION_INCIRCLE");
348    } else {
349        if *accum_len == 0 {
350            accum[..term.len()].copy_from_slice(term);
351            *accum_len = term.len();
352            return;
353        }
354        let sum_len = expansion_sum(&accum[..*accum_len], term, temp)
355            .expect("temp sized for MAX_EXPANSION_INCIRCLE");
356        *accum_len = compress_expansion(&temp[..sum_len], accum)
357            .expect("accum sized for MAX_EXPANSION_INCIRCLE");
358    }
359}
360
361// ──────────────────────────────────────────────────────────────────────────
362//  Public ladder entry point
363// ──────────────────────────────────────────────────────────────────────────
364
365/// The 2-D in-circle predicate: side of `d` w.r.t. the oriented circle
366/// through `a, b, c`.
367///
368/// Returns [`Sign::Positive`] if `d` is inside the oriented circle (when
369/// `a, b, c` are CCW), [`Sign::Zero`] if `d` is on the circle, [`Sign::Negative`]
370/// if outside. The sense is reversed when `a, b, c` are clockwise.
371///
372/// This is the public ladder entry point — it escalates from filtered to
373/// compensated to exact as needed, never returning an uncertain sign.
374pub fn incircle(a: Point2, b: Point2, c: Point2, d: Point2) -> Sign {
375    let diffs = IncircleDiffs::from_points(a, b, c, d);
376    let perm = diffs.permanent();
377
378    let det = filtered_det(&diffs);
379    if det.abs() > perm * FILTERED_BOUND {
380        return Sign::from_f64(det);
381    }
382
383    let comp = compensated_det(&diffs);
384    if comp.abs() > perm * COMPENSATED_BOUND {
385        return Sign::from_f64(comp);
386    }
387
388    exact_det(&diffs)
389}
390
391// ──────────────────────────────────────────────────────────────────────────
392//  Tests
393// ──────────────────────────────────────────────────────────────────────────
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::specialized_libs::computational_geometry::exact_test_helper::Exact;
399    use crate::specialized_libs::computational_geometry::primitives::orientation_2;
400
401    /// Ground-truth in-circle sign via BigInt.
402    ///
403    /// Computes coordinate differences in f64 first (matching the predicate's
404    /// approach), then converts those f64 differences to exact BigInt values
405    /// and computes the determinant exactly. This ensures the cross-check
406    /// validates the same computation the predicate performs.
407    fn exact_incircle_sign(a: Point2, b: Point2, c: Point2, d: Point2) -> Sign {
408        // Compute differences in f64 — same as the predicate.
409        let adx_f = a.x - d.x;
410        let ady_f = a.y - d.y;
411        let bdx_f = b.x - d.x;
412        let bdy_f = b.y - d.y;
413        let cdx_f = c.x - d.x;
414        let cdy_f = c.y - d.y;
415
416        // Convert to exact BigInt values.
417        let adx = Exact::from_f64(adx_f);
418        let ady = Exact::from_f64(ady_f);
419        let bdx = Exact::from_f64(bdx_f);
420        let bdy = Exact::from_f64(bdy_f);
421        let cdx = Exact::from_f64(cdx_f);
422        let cdy = Exact::from_f64(cdy_f);
423
424        let ad2 = adx
425            .clone()
426            .mul(adx.clone())
427            .add(ady.clone().mul(ady.clone()));
428        let bd2 = bdx
429            .clone()
430            .mul(bdx.clone())
431            .add(bdy.clone().mul(bdy.clone()));
432        let cd2 = cdx
433            .clone()
434            .mul(cdx.clone())
435            .add(cdy.clone().mul(cdy.clone()));
436
437        // det = adx*bdy*cd2 - adx*cdy*bd2 - ady*bdx*cd2 + ady*cdx*bd2 + ad2*bdx*cdy - ad2*bdy*cdx
438        let t1 = adx.clone().mul(bdy.clone()).mul(cd2.clone());
439        let t2 = adx.clone().mul(cdy.clone()).mul(bd2.clone());
440        let t3 = ady.clone().mul(bdx.clone()).mul(cd2.clone());
441        let t4 = ady.clone().mul(cdx.clone()).mul(bd2.clone());
442        let t5 = ad2.clone().mul(bdx.clone()).mul(cdy.clone());
443        let t6 = ad2.mul(bdy).mul(cdx);
444
445        let det = t1.sub(t2).sub(t3).add(t4).add(t5).sub(t6);
446        det.sign()
447    }
448
449    // ── Basic classification ──────────────────────────────────────────────
450
451    #[test]
452    fn classifies_inside_circle() {
453        // Circle centered at origin, radius 1. a,b,c on circle (CCW), d at center.
454        let a = Point2::new(1.0, 0.0);
455        let b = Point2::new(0.0, 1.0);
456        let c = Point2::new(-1.0, 0.0);
457        let d = Point2::new(0.0, 0.0);
458        assert_eq!(orientation_2(a, b, c), crate::specialized_libs::computational_geometry::primitives::Orientation::CounterClockwise);
459        assert_eq!(incircle(a, b, c, d), Sign::Positive); // inside
460    }
461
462    #[test]
463    fn classifies_outside_circle() {
464        let a = Point2::new(1.0, 0.0);
465        let b = Point2::new(0.0, 1.0);
466        let c = Point2::new(-1.0, 0.0);
467        let d = Point2::new(2.0, 0.0); // outside the unit circle
468        assert_eq!(incircle(a, b, c, d), Sign::Negative);
469    }
470
471    #[test]
472    fn classifies_on_circle() {
473        let a = Point2::new(1.0, 0.0);
474        let b = Point2::new(0.0, 1.0);
475        let c = Point2::new(-1.0, 0.0);
476        let d = Point2::new(0.0, -1.0); // on the unit circle
477        assert_eq!(incircle(a, b, c, d), Sign::Zero);
478    }
479
480    #[test]
481    fn sign_flips_for_clockwise_abc() {
482        // Same circle but a,b,c clockwise → sign flips
483        let a = Point2::new(1.0, 0.0);
484        let b = Point2::new(-1.0, 0.0);
485        let c = Point2::new(0.0, 1.0);
486        let d = Point2::new(0.0, 0.0); // inside
487        assert_eq!(
488            orientation_2(a, b, c),
489            crate::specialized_libs::computational_geometry::primitives::Orientation::Clockwise
490        );
491        // Inside with CW → Negative (flipped)
492        assert_eq!(incircle(a, b, c, d), Sign::Negative);
493    }
494
495    // ── Agreement with BigInt cross-check ─────────────────────────────────
496
497    #[test]
498    fn agrees_with_exact_on_basic_cases() {
499        let cases = [
500            (
501                Point2::new(1.0, 0.0),
502                Point2::new(0.0, 1.0),
503                Point2::new(-1.0, 0.0),
504                Point2::new(0.0, 0.0),
505            ),
506            (
507                Point2::new(1.0, 0.0),
508                Point2::new(0.0, 1.0),
509                Point2::new(-1.0, 0.0),
510                Point2::new(2.0, 0.0),
511            ),
512            (
513                Point2::new(1.0, 0.0),
514                Point2::new(0.0, 1.0),
515                Point2::new(-1.0, 0.0),
516                Point2::new(0.0, -1.0),
517            ),
518            (
519                Point2::new(0.0, 0.0),
520                Point2::new(1.0, 0.0),
521                Point2::new(0.0, 1.0),
522                Point2::new(0.5, 0.5),
523            ),
524            (
525                Point2::new(3.0, 4.0),
526                Point2::new(0.0, 0.0),
527                Point2::new(6.0, 0.0),
528                Point2::new(3.0, 0.0),
529            ),
530        ];
531        for (a, b, c, d) in cases {
532            assert_eq!(
533                incircle(a, b, c, d),
534                exact_incircle_sign(a, b, c, d),
535                "mismatch on ({a:?}, {b:?}, {c:?}, {d:?})"
536            );
537        }
538    }
539
540    // ── Adversarial: cocircular (exact zero) ──────────────────────────────
541
542    #[test]
543    fn cocircular_four_points() {
544        // Four points on the unit circle
545        let a = Point2::new(1.0, 0.0);
546        let b = Point2::new(0.0, 1.0);
547        let c = Point2::new(-1.0, 0.0);
548        let d = Point2::new(0.0, -1.0);
549        assert_eq!(incircle(a, b, c, d), Sign::Zero);
550        assert_eq!(exact_incircle_sign(a, b, c, d), Sign::Zero);
551    }
552
553    #[test]
554    fn cocircular_on_arbitrary_circle() {
555        // Circle centered at (3, 4), radius 5. Points: (8,4), (3,9), (-2,4), (3,-1)
556        let a = Point2::new(8.0, 4.0);
557        let b = Point2::new(3.0, 9.0);
558        let c = Point2::new(-2.0, 4.0);
559        let d = Point2::new(3.0, -1.0);
560        assert_eq!(incircle(a, b, c, d), Sign::Zero);
561        assert_eq!(exact_incircle_sign(a, b, c, d), Sign::Zero);
562    }
563
564    // ── Adversarial: near-cocircular (±1-ulp) ─────────────────────────────
565
566    #[test]
567    fn near_cocircular_1ulp_off() {
568        // Start cocircular, perturb d by a few ulps.
569        let a = Point2::new(1.0, 0.0);
570        let b = Point2::new(0.0, 1.0);
571        let c = Point2::new(-1.0, 0.0);
572        let d0 = Point2::new(0.0, -1.0); // on circle
573
574        for &delta_bits in &[1i64, -1, 2, -2, 5, -5, 100, -100] {
575            let dy = f64::from_bits((d0.y.to_bits() as i64 + delta_bits) as u64);
576            let d = Point2::new(d0.x, dy);
577            assert_eq!(
578                incircle(a, b, c, d),
579                exact_incircle_sign(a, b, c, d),
580                "mismatch on near-cocircular delta_bits={delta_bits}"
581            );
582        }
583    }
584
585    // ── Adversarial: extreme exponents ────────────────────────────────────
586
587    #[test]
588    fn extreme_exponents_agree_with_exact() {
589        // Use coordinates where intermediate products (coord × coord × sq_dist)
590        // stay within f64 range. With coords ~1e50, sq_dist ~1e100, and triple
591        // products ~1e200, we're well within f64's ~1e308 max.
592        let cases = [
593            (
594                Point2::new(1e50, 0.0),
595                Point2::new(0.0, 1e50),
596                Point2::new(-1e50, 0.0),
597                Point2::new(0.0, 0.0),
598            ),
599            (
600                Point2::new(1e-50, 0.0),
601                Point2::new(0.0, 1e-50),
602                Point2::new(-1e-50, 0.0),
603                Point2::new(0.0, 0.0),
604            ),
605            (
606                Point2::new(1e50, 0.0),
607                Point2::new(0.0, 1e50),
608                Point2::new(-1e50, 0.0),
609                Point2::new(1e50, 1e-50),
610            ),
611        ];
612        for (a, b, c, d) in cases {
613            assert_eq!(
614                incircle(a, b, c, d),
615                exact_incircle_sign(a, b, c, d),
616                "mismatch on extreme-exponent case ({a:?}, {b:?}, {c:?}, {d:?})"
617            );
618        }
619    }
620
621    // ── Adversarial: cancellation ─────────────────────────────────────────
622
623    #[test]
624    fn cancellation_agrees_with_exact() {
625        // Large coordinates with near-cancellation in the determinant.
626        // Keep triple products within f64 range: 1e50³ = 1e150.
627        let a = Point2::new(1e50, 0.0);
628        let b = Point2::new(0.0, 1e50);
629        let c = Point2::new(-1e50, 0.0);
630        let d = Point2::new(0.0, -1e50); // cocircular
631        assert_eq!(incircle(a, b, c, d), exact_incircle_sign(a, b, c, d));
632    }
633
634    #[test]
635    fn massive_cancellation_agrees_with_exact() {
636        // Points on a huge circle, d perturbed slightly.
637        let a = Point2::new(1e50, 0.0);
638        let b = Point2::new(0.0, 1e50);
639        let c = Point2::new(-1e50, 0.0);
640        // d just inside the circle — the determinant involves massive cancellation
641        let d = Point2::new(0.0, -1e50 + 1.0);
642        assert_eq!(
643            incircle(a, b, c, d),
644            exact_incircle_sign(a, b, c, d),
645            "mismatch on massive cancellation"
646        );
647    }
648
649    // ── All three ladder stages exercised ─────────────────────────────────
650
651    #[test]
652    fn filtered_stage_resolves_clear_case() {
653        let a = Point2::new(1.0, 0.0);
654        let b = Point2::new(0.0, 1.0);
655        let c = Point2::new(-1.0, 0.0);
656        let d = Point2::new(0.0, 0.0);
657        let diffs = IncircleDiffs::from_points(a, b, c, d);
658        let det = filtered_det(&diffs);
659        let perm = diffs.permanent();
660        assert!(
661            det.abs() > perm * FILTERED_BOUND,
662            "filtered should resolve (det={det}, bound={})",
663            perm * FILTERED_BOUND
664        );
665        assert_eq!(incircle(a, b, c, d), Sign::Positive);
666    }
667
668    #[test]
669    fn near_cocircular_resolves_via_compensated_or_exact() {
670        let a = Point2::new(1.0, 0.0);
671        let b = Point2::new(0.0, 1.0);
672        let c = Point2::new(-1.0, 0.0);
673        let d = Point2::new(0.0, -1.0 + 1e-15);
674        assert_eq!(
675            incircle(a, b, c, d),
676            exact_incircle_sign(a, b, c, d),
677            "near-cocircular must match exact"
678        );
679    }
680
681    // ── Determinism ───────────────────────────────────────────────────────
682
683    #[test]
684    fn deterministic_across_calls() {
685        let a = Point2::new(1.0, 0.0);
686        let b = Point2::new(0.0, 1.0);
687        let c = Point2::new(-1.0, 0.0);
688        let d = Point2::new(0.3, 0.3);
689        let s1 = incircle(a, b, c, d);
690        let s2 = incircle(a, b, c, d);
691        assert_eq!(s1, s2);
692    }
693
694    // ── Symmetry ──────────────────────────────────────────────────────────
695
696    #[test]
697    fn swapping_a_b_flips_sign() {
698        let a = Point2::new(1.0, 0.0);
699        let b = Point2::new(0.0, 1.0);
700        let c = Point2::new(-1.0, 0.0);
701        let d = Point2::new(0.0, 0.0);
702        let s = incircle(a, b, c, d);
703        let s_swapped = incircle(b, a, c, d);
704        assert_eq!(s, s_swapped.flip());
705    }
706
707    #[test]
708    fn translation_invariant() {
709        let a = Point2::new(1.0, 0.0);
710        let b = Point2::new(0.0, 1.0);
711        let c = Point2::new(-1.0, 0.0);
712        let d = Point2::new(0.0, 0.0);
713        let t = Point2::new(1e10, -1e10);
714        let s = incircle(a, b, c, d);
715        let s_t = incircle(
716            Point2::new(a.x + t.x, a.y + t.y),
717            Point2::new(b.x + t.x, b.y + t.y),
718            Point2::new(c.x + t.x, c.y + t.y),
719            Point2::new(d.x + t.x, d.y + t.y),
720        );
721        assert_eq!(s, s_t);
722    }
723
724    // ── Zero-heap contract ────────────────────────────────────────────────
725
726    #[test]
727    fn no_heap_allocation_in_predicate() {
728        let a = Point2::new(1.0, 0.0);
729        let b = Point2::new(0.0, 1.0);
730        let c = Point2::new(-1.0, 0.0);
731        let d = Point2::new(0.0, -1.0);
732        let _ = incircle(a, b, c, d); // cocircular — exercises exact stage
733    }
734}