Skip to main content

axiolid_predicates/
sphere.rs

1//! `incircle` and `insphere`: is a point inside a circumscribed ball?
2//!
3//! These are the Delaunay predicates. `incircle(a, b, c, d)` asks whether `d`
4//! lies inside the circle through `a`, `b`, `c`; `insphere` is the 3D analogue.
5//! A zero means `d` lies exactly on the ball -- the cocircular/cospherical case
6//! that makes a Delaunay triangulation non-unique and, if misjudged, produces
7//! inverted or overlapping cells.
8//!
9//! Both are lifted determinants: adding a coordinate equal to the squared
10//! distance from the origin turns "inside a ball" into "below a hyperplane".
11//! That lift squares the operand magnitudes, so the error bound grows faster
12//! than `orient*`'s and the filter fails sooner -- which is why the escalation
13//! rate is measured rather than assumed.
14//!
15//! The determinants have degree 4 (`incircle`) and 5 (`insphere`) in the
16//! coordinate differences, so their products leave binary64's range much
17//! sooner than `orient*`'s (#430). The filter and the expansion stage of the
18//! exact path therefore run only where every nonzero difference lies in a
19//! range that keeps all products normal: [2^-128, 2^128) for the filters, about
20//! [1e-60, 1e60] for the `incircle` expansion stage and [1e-45, 1e45] for the
21//! `insphere` one; the expansion stage also needs every nonzero
22//! rounding tail of a difference at least as large as the range's lower
23//! bound. Any
24//! other finite input goes to a fixed-size dyadic evaluation over the original
25//! coordinates (`sphere_dyadic`), which forms no difference and no rounded
26//! product and is exact for every finite input. A non-finite coordinate returns
27//! [`Certified::Uncertain`].
28
29use axiolid_core::{Point2, Point3};
30use axiolid_guarantees::{Certified, Precision, Sign};
31
32use crate::arithmetic::{expansion_product, expansion_sign, expansion_sum, negate_expansion};
33use crate::expansion::two_diff;
34use crate::orient3::{highest_bit_exponent, least_significant_bit_exponent};
35use crate::sphere_dyadic::{incircle_exact_dyadic, insphere_exact_dyadic};
36
37/// Machine epsilon for binary64.
38const EPSILON: f64 = f64::EPSILON / 2.0;
39
40/// Relative error bound for the lifted 3x3 `incircle` determinant.
41const INCIRCLE_ERROR_FACTOR: f64 = (10.0 + 96.0 * EPSILON) * EPSILON;
42
43/// Relative error bound for the lifted 4x4 `insphere` determinant.
44const INSPHERE_ERROR_FACTOR: f64 = (16.0 + 224.0 * EPSILON) * EPSILON;
45
46/// Differences of `incircle` must be zero or within `[INCIRCLE_MIN, INCIRCLE_MAX]`
47/// in magnitude for the filter and the expansion stage: a product of four of
48/// them, and every partial product, then lies in `[1e-240, 1e240]`. Gating the
49/// permanent instead is not enough: a degree-2 sub-product can underflow and
50/// then be multiplied by a large lift, dropping a whole term.
51const INCIRCLE_MIN: f64 = 1.0e-60;
52const INCIRCLE_MAX: f64 = 1.0e60;
53
54/// The same range for `insphere`, degree 5: products in `[1e-225, 1e225]`.
55const INSPHERE_MIN: f64 = 1.0e-45;
56const INSPHERE_MAX: f64 = 1.0e45;
57
58/// Whether every value is zero or has a magnitude in `[min, max]`; a NaN or
59/// an infinity is not.
60fn all_in_range(values: &[f64], min: f64, max: f64) -> bool {
61    // Bitwise `&` and `|` keep this free of branches, which the filter pays on every call.
62    values.iter().fold(true, |ok, v| {
63        let magnitude = v.abs();
64        ok & ((magnitude == 0.0) | ((magnitude >= min) & (magnitude <= max)))
65    })
66}
67
68/// Whether every `p - q` is in range for the expansion stage: its head is
69/// zero or in `[min, max]` and its rounding tail is zero or at least `min`,
70/// so that no product of parts, nor its own error term, underflows.
71fn differences_expandable(pairs: &[(f64, f64)], min: f64, max: f64) -> bool {
72    pairs.iter().all(|&(p, q)| {
73        let (head, tail) = two_diff(p, q);
74        all_in_range(&[head], min, max) && (tail == 0.0 || tail.abs() >= min)
75    })
76}
77
78/// The filter's differences must be zero or have a biased exponent in
79/// `[FILTER_EXPONENT_LOW, FILTER_EXPONENT_LOW + 255]`, that is a magnitude in
80/// `[2^-128, 2^128)`: every product of four (`incircle`) or five (`insphere`)
81/// of them is then in `[2^-640, 2^640)`, far from both ends of binary64.
82const FILTER_EXPONENT_LOW: u64 = 1023 - 128;
83
84/// Whether every value is zero or in the filter's exponent window; a
85/// subnormal, an infinity and a NaN are not. Branch-free: the filter pays it
86/// on every call, so it reads the exponent bits and checks the OR of all
87/// window offsets once (the window is a power of two wide).
88fn exponents_in_range(values: &[f64]) -> bool {
89    let offsets = values.iter().fold(0_u64, |or, v| {
90        let bits = v.to_bits();
91        let nonzero = u64::from(bits << 1 != 0).wrapping_neg();
92        or | ((((bits >> 52) & 0x7ff).wrapping_sub(FILTER_EXPONENT_LOW)) & nonzero)
93    });
94    offsets >> 8 == 0
95}
96
97/// Is `d` inside the circle through `a`, `b`, `c`?
98///
99/// [`Sign::Positive`] means inside when `a, b, c` are counter-clockwise.
100/// Callers that cannot guarantee that orientation must normalise it first with
101/// `orient2d`, because the sign of this determinant flips with it.
102///
103/// [`Certified::Certain`] for every finite input, exactly (#430); a
104/// non-finite coordinate returns [`Certified::Uncertain`].
105#[must_use]
106pub fn incircle(a: Point2, b: Point2, c: Point2, d: Point2) -> Certified {
107    match incircle_filter(a, b, c, d) {
108        Certified::Certain { sign, .. } => Certified::exact_sign(sign),
109        _ => incircle_exact(a, b, c, d).map_or(
110            Certified::Uncertain {
111                attempted: Precision::Exact,
112            },
113            Certified::exact_sign,
114        ),
115    }
116}
117
118/// The fast filter alone, exposed so escalation can be measured.
119///
120/// Returns [`Certified::Uncertain`] when a nonzero coordinate difference is
121/// outside [2^-128, 2^128), where a product may overflow or underflow.
122#[must_use]
123pub fn incircle_filter(a: Point2, b: Point2, c: Point2, d: Point2) -> Certified {
124    let (adx, ady) = (a.x - d.x, a.y - d.y);
125    let (bdx, bdy) = (b.x - d.x, b.y - d.y);
126    let (cdx, cdy) = (c.x - d.x, c.y - d.y);
127    if !exponents_in_range(&[adx, ady, bdx, bdy, cdx, cdy]) {
128        return Certified::Uncertain {
129            attempted: Precision::F64,
130        };
131    }
132    let bdxcdy = bdx * cdy;
133    let cdxbdy = cdx * bdy;
134    let alift = adx * adx + ady * ady;
135
136    let cdxady = cdx * ady;
137    let adxcdy = adx * cdy;
138    let blift = bdx * bdx + bdy * bdy;
139
140    let adxbdy = adx * bdy;
141    let bdxady = bdx * ady;
142    let clift = cdx * cdx + cdy * cdy;
143
144    let determinant =
145        alift * (bdxcdy - cdxbdy) + blift * (cdxady - adxcdy) + clift * (adxbdy - bdxady);
146
147    let permanent = (bdxcdy.abs() + cdxbdy.abs()) * alift
148        + (cdxady.abs() + adxcdy.abs()) * blift
149        + (adxbdy.abs() + bdxady.abs()) * clift;
150
151    Certified::from_filter(
152        determinant,
153        INCIRCLE_ERROR_FACTOR * permanent,
154        Precision::F64,
155    )
156}
157
158/// Exact sign of the lifted `incircle` determinant.
159///
160/// Every coordinate difference is kept exactly, as a two-term expansion
161/// (`two_diff`), and every product and sum after it is an expansion. The
162/// differences used to be rounded first, which made this "exact" fallback
163/// wrong exactly where the filter hands over -- nearly cocircular points
164/// whose differences do not fit an `f64` -- and Delaunay flips driven by it
165/// could cycle for ever (#190).
166///
167/// Only where every difference and rounding tail is in range; elsewhere, and
168/// for every finite input beyond it, the dyadic evaluation decides (#430).
169/// `None` for a non-finite coordinate.
170#[must_use]
171fn incircle_exact(a: Point2, b: Point2, c: Point2, d: Point2) -> Option<Sign> {
172    // A non-finite coordinate makes a head NaN or infinite, so it is never
173    // expandable, and the dyadic evaluation refuses it.
174    let pairs = [
175        (a.x, d.x),
176        (a.y, d.y),
177        (b.x, d.x),
178        (b.y, d.y),
179        (c.x, d.x),
180        (c.y, d.y),
181    ];
182    if differences_expandable(&pairs, INCIRCLE_MIN, INCIRCLE_MAX) {
183        Some(incircle_expansion(a, b, c, d))
184    } else {
185        incircle_exact_dyadic(a, b, c, d)
186    }
187}
188
189/// The expansion-arithmetic tier of [`incircle_exact`].
190#[must_use]
191fn incircle_expansion(a: Point2, b: Point2, c: Point2, d: Point2) -> Sign {
192    let (adx, ady) = (diff(a.x, d.x), diff(a.y, d.y));
193    let (bdx, bdy) = (diff(b.x, d.x), diff(b.y, d.y));
194    let (cdx, cdy) = (diff(c.x, d.x), diff(c.y, d.y));
195
196    let bc = minor(&bdx, &cdy, &cdx, &bdy);
197    let ca = minor(&cdx, &ady, &adx, &cdy);
198    let ab = minor(&adx, &bdy, &bdx, &ady);
199
200    let total = expansion_sum(
201        &expansion_sum(&lift(&bc, &adx, &ady), &lift(&ca, &bdx, &bdy)),
202        &lift(&ab, &cdx, &cdy),
203    );
204    expansion_sign(&total)
205}
206
207/// `p - q`, exactly, as an expansion.
208#[must_use]
209fn diff(p: f64, q: f64) -> Vec<f64> {
210    let (d, err) = two_diff(p, q);
211    let mut e = Vec::with_capacity(2);
212    if err != 0.0 {
213        e.push(err);
214    }
215    if d != 0.0 || e.is_empty() {
216        e.push(d);
217    }
218    e
219}
220
221/// `p q - r s` over expansions, exactly.
222#[must_use]
223fn minor(p: &[f64], q: &[f64], r: &[f64], s: &[f64]) -> Vec<f64> {
224    expansion_sum(
225        &expansion_product(p, q),
226        &negate_expansion(&expansion_product(r, s)),
227    )
228}
229
230/// Multiply an expansion by `x*x + y*y`, exactly.
231#[must_use]
232fn lift(e: &[f64], x: &[f64], y: &[f64]) -> Vec<f64> {
233    let square = expansion_sum(&expansion_product(x, x), &expansion_product(y, y));
234    expansion_product(e, &square)
235}
236
237/// Is `e` inside the sphere through `a`, `b`, `c`, `d`?
238///
239/// [`Sign::Positive`] means inside when `a, b, c, d` are positively oriented
240/// (`orient3d(a, b, c, d) > 0`). As with [`incircle`], the sign flips with the
241/// base orientation, so a caller must normalise it.
242///
243/// [`Certified::Certain`] for every finite input, exactly (#430); a
244/// non-finite coordinate returns [`Certified::Uncertain`].
245#[must_use]
246pub fn insphere(a: Point3, b: Point3, c: Point3, d: Point3, e: Point3) -> Certified {
247    match insphere_filter(a, b, c, d, e) {
248        Certified::Certain { sign, .. } => Certified::exact_sign(sign),
249        _ => insphere_exact(a, b, c, d, e).map_or(
250            Certified::Uncertain {
251                attempted: Precision::Exact,
252            },
253            Certified::exact_sign,
254        ),
255    }
256}
257
258/// The fast filter alone, exposed so escalation can be measured.
259///
260/// Returns [`Certified::Uncertain`] when a nonzero coordinate difference is
261/// outside [2^-128, 2^128), where a product may overflow or underflow.
262#[must_use]
263pub fn insphere_filter(a: Point3, b: Point3, c: Point3, d: Point3, e: Point3) -> Certified {
264    let v = |p: Point3| (p.x - e.x, p.y - e.y, p.z - e.z);
265    let (ax, ay, az) = v(a);
266    let (bx, by, bz) = v(b);
267    let (cx, cy, cz) = v(c);
268    let (dx, dy, dz) = v(d);
269    if !exponents_in_range(&[ax, ay, az, bx, by, bz, cx, cy, cz, dx, dy, dz]) {
270        return Certified::Uncertain {
271            attempted: Precision::F64,
272        };
273    }
274    let (axby, bxay) = (ax * by, bx * ay);
275    let (bxcy, cxby) = (bx * cy, cx * by);
276    let (cxdy, dxcy) = (cx * dy, dx * cy);
277    let (dxay, axdy) = (dx * ay, ax * dy);
278    let (axcy, cxay) = (ax * cy, cx * ay);
279    let (bxdy, dxby) = (bx * dy, dx * by);
280    let ab = axby - bxay;
281    let bc = bxcy - cxby;
282    let cd = cxdy - dxcy;
283    let da = dxay - axdy;
284    let ac = axcy - cxay;
285    let bd = bxdy - dxby;
286
287    let abc = az * bc - bz * ac + cz * ab;
288    let bcd = bz * cd - cz * bd + dz * bc;
289    let cda = cz * da + dz * ac + az * cd;
290    let dab = dz * ab + az * bd + bz * da;
291
292    let alift = ax * ax + ay * ay + az * az;
293    let blift = bx * bx + by * by + bz * bz;
294    let clift = cx * cx + cy * cy + cz * cz;
295    let dlift = dx * dx + dy * dy + dz * dz;
296
297    let determinant = (dlift * abc - clift * dab) + (blift * cda - alift * bcd);
298
299    // Shewchuk's permanent: the same expression over the ABSOLUTE values of
300    // every elementary product. Bounding by |abc| and friends instead -- the
301    // rounded minors -- is not a bound at all: when a minor cancels, its
302    // rounding error is not proportional to its value, and the filter
303    // certified a non-zero sign for exactly cospherical points (#126).
304    let (az, bz, cz, dz) = (az.abs(), bz.abs(), cz.abs(), dz.abs());
305    let ab_plus = axby.abs() + bxay.abs();
306    let bc_plus = bxcy.abs() + cxby.abs();
307    let cd_plus = cxdy.abs() + dxcy.abs();
308    let da_plus = dxay.abs() + axdy.abs();
309    let ac_plus = axcy.abs() + cxay.abs();
310    let bd_plus = bxdy.abs() + dxby.abs();
311    let permanent = (cd_plus * bz + bd_plus * cz + bc_plus * dz) * alift
312        + (da_plus * cz + ac_plus * dz + cd_plus * az) * blift
313        + (ab_plus * dz + bd_plus * az + da_plus * bz) * clift
314        + (bc_plus * az + ac_plus * bz + ab_plus * cz) * dlift;
315
316    Certified::from_filter(
317        determinant,
318        INSPHERE_ERROR_FACTOR * permanent,
319        Precision::F64,
320    )
321}
322
323/// Exact sign of the lifted 4x4 `insphere` determinant.
324///
325/// Expands along the lifted column: each 3x3 minor is built from exact 2x2
326/// cofactors, scaled by the remaining z difference, then by the squared
327/// distance. Nothing is rounded between those steps.
328///
329/// The expansion stage runs only where every difference and rounding tail is
330/// in range; elsewhere the dyadic evaluation decides (#430). `None` for a
331/// non-finite coordinate.
332#[must_use]
333fn insphere_exact(a: Point3, b: Point3, c: Point3, d: Point3, e: Point3) -> Option<Sign> {
334    // As for `incircle_exact`, a non-finite coordinate is neither a small
335    // integer nor expandable, and the dyadic evaluation refuses it.
336    if let Some(sign) = insphere_small_integer(a, b, c, d, e) {
337        return Some(sign);
338    }
339    let pairs = [a, b, c, d].map(|p| [(p.x, e.x), (p.y, e.y), (p.z, e.z)]);
340    if differences_expandable(pairs.as_flattened(), INSPHERE_MIN, INSPHERE_MAX) {
341        Some(insphere_expansion(a, b, c, d, e))
342    } else {
343        insphere_exact_dyadic(a, b, c, d, e)
344    }
345}
346
347/// The expansion-arithmetic tier of [`insphere_exact`].
348#[must_use]
349fn insphere_expansion(a: Point3, b: Point3, c: Point3, d: Point3, e: Point3) -> Sign {
350    // Differences exact, as for `incircle_exact` (#190).
351    let v = |p: Point3| [diff(p.x, e.x), diff(p.y, e.y), diff(p.z, e.z)];
352    let (a3, b3, c3, d3) = (v(a), v(b), v(c), v(d));
353
354    let minor3 = |p: &[Vec<f64>; 3], q: &[Vec<f64>; 3], r: &[Vec<f64>; 3]| {
355        let qr = minor(&q[0], &r[1], &r[0], &q[1]);
356        let rp = minor(&r[0], &p[1], &p[0], &r[1]);
357        let pq = minor(&p[0], &q[1], &q[0], &p[1]);
358        expansion_sum(
359            &expansion_sum(
360                &expansion_product(&qr, &p[2]),
361                &expansion_product(&rp, &q[2]),
362            ),
363            &expansion_product(&pq, &r[2]),
364        )
365    };
366
367    let bcd = minor3(&b3, &c3, &d3);
368    let cda = minor3(&c3, &d3, &a3);
369    let dab = minor3(&d3, &a3, &b3);
370    let abc = minor3(&a3, &b3, &c3);
371
372    // Cofactor expansion signs alternate: +d -c +b -a.
373    let total = expansion_sum(
374        &expansion_sum(&lift3(&abc, &d3), &negate_expansion(&lift3(&dab, &c3))),
375        &expansion_sum(&lift3(&cda, &b3), &negate_expansion(&lift3(&bcd, &a3))),
376    );
377    expansion_sign(&total)
378}
379
380/// Exact `insphere` in `i128`, for differences on a narrow dyadic grid.
381///
382/// Exactly degenerate inputs -- a cubic lattice, where every insphere test
383/// is an exact zero -- reach the exact path on every call, and the
384/// expansion arithmetic allocates throughout. When every coordinate
385/// difference is itself exact in `f64` and all are integer multiples of one
386/// power of two `2^k` with magnitude below `2^(k + 20)`, the determinant of
387/// the scaled integers is at most `72 * 2^100` in magnitude and fits `i128`
388/// exactly. Returns `None` (use the expansions) otherwise.
389#[must_use]
390fn insphere_small_integer(a: Point3, b: Point3, c: Point3, d: Point3, e: Point3) -> Option<Sign> {
391    let mut differences = [[0.0f64; 3]; 4];
392    for (row, p) in [a, b, c, d].into_iter().enumerate() {
393        for (axis, (x, y)) in [(p.x, e.x), (p.y, e.y), (p.z, e.z)].into_iter().enumerate() {
394            let (difference, error) = two_diff(x, y);
395            if error != 0.0 || !difference.is_finite() {
396                return None;
397            }
398            differences[row][axis] = difference;
399        }
400    }
401    let nonzero = || differences.iter().flatten().filter(|v| **v != 0.0);
402    let Some(lowest) = nonzero().map(|v| least_significant_bit_exponent(*v)).min() else {
403        return Some(Sign::Zero);
404    };
405    let highest = nonzero().map(|v| highest_bit_exponent(*v)).max()?;
406    if highest - lowest >= 20 {
407        return None;
408    }
409    let scaled = |v: f64| -> i128 {
410        if v == 0.0 {
411            return 0;
412        }
413        // |v| = significand * 2^exponent exactly; shift onto the 2^lowest grid.
414        let bits = v.abs().to_bits();
415        let encoded = ((bits >> 52) & 0x7ff) as i32;
416        let fraction = bits & ((1u64 << 52) - 1);
417        let (significand, exponent) = if encoded == 0 {
418            (fraction, -1074)
419        } else {
420            (fraction | (1u64 << 52), encoded - 1075)
421        };
422        let shift = exponent - lowest;
423        let magnitude = if shift >= 0 {
424            i128::from(significand) << shift
425        } else {
426            i128::from(significand >> -shift)
427        };
428        if v < 0.0 {
429            -magnitude
430        } else {
431            magnitude
432        }
433    };
434    let row = |r: usize| {
435        let [x, y, z] = differences[r].map(scaled);
436        [x, y, z, x * x + y * y + z * z]
437    };
438    let m = [row(0), row(1), row(2), row(3)];
439    let det3 = |p: [i128; 4], q: [i128; 4], r: [i128; 4]| {
440        p[0] * (q[1] * r[2] - r[1] * q[2]) - p[1] * (q[0] * r[2] - r[0] * q[2])
441            + p[2] * (q[0] * r[1] - r[0] * q[1])
442    };
443    // The lifted determinant with the sign convention of `insphere`.
444    let total = -m[0][3] * det3(m[1], m[2], m[3]) + m[1][3] * det3(m[0], m[2], m[3])
445        - m[2][3] * det3(m[0], m[1], m[3])
446        + m[3][3] * det3(m[0], m[1], m[2]);
447    Some(match total.signum() {
448        1 => Sign::Positive,
449        -1 => Sign::Negative,
450        _ => Sign::Zero,
451    })
452}
453
454/// Multiply an expansion by `x*x + y*y + z*z`, exactly.
455#[must_use]
456fn lift3(e: &[f64], p: &[Vec<f64>; 3]) -> Vec<f64> {
457    let square = expansion_sum(
458        &expansion_sum(
459            &expansion_product(&p[0], &p[0]),
460            &expansion_product(&p[1], &p[1]),
461        ),
462        &expansion_product(&p[2], &p[2]),
463    );
464    expansion_product(e, &square)
465}
466
467/// Smallest exponent `k` with `2^k` a coordinate magnitude the exact path of
468/// [`in_diametral_sphere`] accepts; zero is always accepted.
469const DIAMETRAL_MIN_EXPONENT: i32 = -100;
470
471/// Largest such exponent.
472const DIAMETRAL_MAX_EXPONENT: i32 = 100;
473
474/// Is `d` inside the diametral sphere of the triangle `a`, `b`, `c`?
475///
476/// The diametral sphere is the smallest sphere through `a`, `b`, `c`: its
477/// centre is the triangle's circumcentre and its equator the circumcircle.
478/// For `d` coplanar with the triangle this is therefore the coplanar
479/// `incircle` test in 3D -- the question a 3D Delaunay triangulation asks of
480/// a point lying exactly in the plane of a convex-hull face -- and, unlike
481/// [`insphere`], it needs no orientation: [`Sign::Positive`] means strictly
482/// inside, [`Sign::Negative`] strictly outside, [`Sign::Zero`] on the sphere.
483///
484/// The sign is that of
485///
486/// ```text
487/// -( |n|^2 (w.w) - |u|^2 ((w x v).n) - |v|^2 ((u x w).n) )
488/// ```
489///
490/// with `u = b - a`, `v = c - a`, `w = d - a` and `n = u x v`: `|n|^2` times
491/// the power of `d` with respect to the sphere, a degree-6 polynomial in the
492/// coordinates.
493///
494/// A degenerate triangle (collinear `a`, `b`, `c`) has no diametral sphere and
495/// returns `Zero`. A forward running-error filter decides almost every case;
496/// the exact expansion path decides the rest. The exact path needs every
497/// non-zero coordinate magnitude in `[2^-100, 2^100]`, so that no product in
498/// the degree-6 determinant leaves binary64's normal range; outside it, and
499/// for non-finite input, the result is [`Certified::Uncertain`] rather than a
500/// guess.
501#[must_use]
502pub fn in_diametral_sphere(a: Point3, b: Point3, c: Point3, d: Point3) -> Certified {
503    match in_diametral_sphere_filter(a, b, c, d) {
504        Certified::Certain { sign, .. } => Certified::exact_sign(sign),
505        _ => {
506            if [a, b, c, d].iter().all(|p| {
507                [p.x, p.y, p.z].iter().all(|&x| {
508                    x == 0.0
509                        || (x.is_finite()
510                            && x.abs() >= 2f64.powi(DIAMETRAL_MIN_EXPONENT)
511                            && x.abs() <= 2f64.powi(DIAMETRAL_MAX_EXPONENT))
512                })
513            }) {
514                Certified::exact_sign(in_diametral_sphere_exact(a, b, c, d))
515            } else {
516                Certified::Uncertain {
517                    attempted: Precision::Exact,
518                }
519            }
520        }
521    }
522}
523
524/// A value and a bound on its absolute error, for a running-error filter.
525#[derive(Clone, Copy)]
526struct Bounded {
527    value: f64,
528    error: f64,
529}
530
531/// Unit roundoff, taken at twice its size so the `1 / (1 - eps)` factor of
532/// the backward bound is absorbed.
533const ROUNDOFF: f64 = f64::EPSILON;
534
535impl Bounded {
536    fn difference(p: f64, q: f64) -> Self {
537        let value = p - q;
538        Self {
539            value,
540            error: ROUNDOFF * value.abs(),
541        }
542    }
543
544    fn add(self, other: Self) -> Self {
545        let value = self.value + other.value;
546        Self {
547            value,
548            error: self.error + other.error + ROUNDOFF * value.abs(),
549        }
550    }
551
552    fn sub(self, other: Self) -> Self {
553        let value = self.value - other.value;
554        Self {
555            value,
556            error: self.error + other.error + ROUNDOFF * value.abs(),
557        }
558    }
559
560    fn mul(self, other: Self) -> Self {
561        let value = self.value * other.value;
562        Self {
563            value,
564            error: self.value.abs() * other.error
565                + other.value.abs() * self.error
566                + self.error * other.error
567                + ROUNDOFF * value.abs(),
568        }
569    }
570}
571
572/// The running-error filter alone, exposed so escalation can be measured.
573///
574/// Every operation carries a bound on its absolute error; the bound itself is
575/// computed in rounded arithmetic, so the final bound is inflated by a
576/// relative margin far larger than the rounding the bound's own evaluation
577/// can incur. A non-zero coordinate difference outside `[2^-150, 2^150]`
578/// (whose degree-6 products could leave the normal range) or a non-finite
579/// value is left [`Certified::Uncertain`].
580#[must_use]
581pub fn in_diametral_sphere_filter(a: Point3, b: Point3, c: Point3, d: Point3) -> Certified {
582    let vector = |p: Point3| {
583        [
584            Bounded::difference(p.x, a.x),
585            Bounded::difference(p.y, a.y),
586            Bounded::difference(p.z, a.z),
587        ]
588    };
589    let (u, v, w) = (vector(b), vector(c), vector(d));
590    let cross = |p: [Bounded; 3], q: [Bounded; 3]| {
591        [
592            p[1].mul(q[2]).sub(p[2].mul(q[1])),
593            p[2].mul(q[0]).sub(p[0].mul(q[2])),
594            p[0].mul(q[1]).sub(p[1].mul(q[0])),
595        ]
596    };
597    let dot =
598        |p: [Bounded; 3], q: [Bounded; 3]| p[0].mul(q[0]).add(p[1].mul(q[1])).add(p[2].mul(q[2]));
599    let n = cross(u, v);
600    let power = dot(n, n)
601        .mul(dot(w, w))
602        .sub(dot(u, u).mul(dot(cross(w, v), n)))
603        .sub(dot(v, v).mul(dot(cross(u, w), n)));
604
605    let tiny = 2f64.powi(-900);
606    let operands = [u, v, w].into_iter().flatten().map(|x| x.value.abs());
607    let representable = operands.clone().all(f64::is_finite)
608        && operands
609            .filter(|x| *x != 0.0)
610            .all(|x| x >= 2f64.powi(-150) && x <= 2f64.powi(150))
611        && power.value.is_finite()
612        && power.error.is_finite();
613    if !representable {
614        return Certified::Uncertain {
615            attempted: Precision::F64,
616        };
617    }
618    // The bound's own evaluation rounds a few dozen times; 2^-40 relative
619    // dwarfs that, and the absolute term covers underflow in the bound.
620    let bound = power.error * (1.0 + 2f64.powi(-40)) + tiny;
621    Certified::from_filter(-power.value, bound, Precision::F64)
622}
623
624/// Exact sign of the diametral-sphere determinant, over expansions.
625#[must_use]
626fn in_diametral_sphere_exact(a: Point3, b: Point3, c: Point3, d: Point3) -> Sign {
627    let vector = |p: Point3| [diff(p.x, a.x), diff(p.y, a.y), diff(p.z, a.z)];
628    let (u, v, w) = (vector(b), vector(c), vector(d));
629    let cross = |p: &[Vec<f64>; 3], q: &[Vec<f64>; 3]| {
630        [
631            minor(&p[1], &q[2], &p[2], &q[1]),
632            minor(&p[2], &q[0], &p[0], &q[2]),
633            minor(&p[0], &q[1], &p[1], &q[0]),
634        ]
635    };
636    let dot = |p: &[Vec<f64>; 3], q: &[Vec<f64>; 3]| {
637        expansion_sum(
638            &expansion_sum(
639                &expansion_product(&p[0], &q[0]),
640                &expansion_product(&p[1], &q[1]),
641            ),
642            &expansion_product(&p[2], &q[2]),
643        )
644    };
645    let n = cross(&u, &v);
646    let first = expansion_product(&dot(&n, &n), &dot(&w, &w));
647    let second = expansion_product(&dot(&u, &u), &dot(&cross(&w, &v), &n));
648    let third = expansion_product(&dot(&v, &v), &dot(&cross(&u, &w), &n));
649    let power = expansion_sum(&first, &negate_expansion(&expansion_sum(&second, &third)));
650    expansion_sign(&power).flip()
651}
652
653#[cfg(test)]
654mod tests {
655    use super::{insphere_expansion, insphere_small_integer};
656    use axiolid_core::Point3;
657
658    fn next(state: &mut u64) -> u64 {
659        *state ^= *state << 13;
660        *state ^= *state >> 7;
661        *state ^= *state << 17;
662        *state
663    }
664
665    /// The integer tier agrees with the expansion tier wherever it answers,
666    /// non-zero signs included (the filter usually settles those, so the
667    /// public predicate alone would not exercise them), and it declines
668    /// every input whose scaled integers could overflow `i128`.
669    #[test]
670    fn the_integer_tier_agrees_with_the_expansions() {
671        let mut state = 0x2545_F491_4F6C_DD1Du64;
672        let (mut answered, mut declined) = (0usize, 0usize);
673        for round in 0..40_000u32 {
674            // Magnitudes from a few units up to 2^30, on grids of 2^-8..2^8.
675            let bits = 2 + round % 29;
676            let unit = 2f64.powi((next(&mut state) % 17) as i32 - 8);
677            let mut coordinate = || {
678                let span = 1u64 << bits;
679                ((next(&mut state) % (2 * span)) as f64 - span as f64) * unit
680            };
681            let mut point = || Point3::new(coordinate(), coordinate(), coordinate());
682            let (a, b, c, d, e) = (point(), point(), point(), point(), point());
683            let expected = insphere_expansion(a, b, c, d, e);
684            match insphere_small_integer(a, b, c, d, e) {
685                Some(sign) => {
686                    assert_eq!(sign, expected, "{a:?} {b:?} {c:?} {d:?} {e:?}");
687                    answered += 1;
688                }
689                None => declined += 1,
690            }
691        }
692        assert!(
693            answered > 10_000 && declined > 10_000,
694            "{answered} / {declined}"
695        );
696    }
697}