#[cfg(test)]
use core::sync::atomic::{AtomicUsize, Ordering};
#[cfg(test)]
pub(crate) static INSPHERE_EXACT_COUNT: AtomicUsize = AtomicUsize::new(0);
use crate::counters::{self, Outcome, Predicate};
use crate::{NonFinite, Sign};
use crate::expansion::{
ISP_ERR_BOUND_A, ISP_ERR_BOUND_B, expansion_sum, scale_expansion, two_product,
};
use crate::range::{Prepared, U_N, U_S, Window, prepare};
use crate::util::{sign_of, two_diff_tail, two_two_diff};
type Point3 = [f64; 3];
fn insphere_filter_raw(pa: Point3, pb: Point3, pc: Point3, pd: Point3, pe: Point3) -> (f64, f64) {
let aex = pa[0] - pe[0];
let aey = pa[1] - pe[1];
let aez = pa[2] - pe[2];
let bex = pb[0] - pe[0];
let bey = pb[1] - pe[1];
let bez = pb[2] - pe[2];
let cex = pc[0] - pe[0];
let cey = pc[1] - pe[1];
let cez = pc[2] - pe[2];
let dex = pd[0] - pe[0];
let dey = pd[1] - pe[1];
let dez = pd[2] - pe[2];
let minor = |p: f64, q: f64, r: f64, s: f64| p * q - r * s;
let minor_mag = |p: f64, q: f64, r: f64, s: f64| ((p * q).abs() + U_N) + ((r * s).abs() + U_N);
let ab = minor(aex, bey, bex, aey);
let bc = minor(bex, cey, cex, bey);
let cd = minor(cex, dey, dex, cey);
let da = minor(dex, aey, aex, dey);
let ac = minor(aex, cey, cex, aey);
let bd = minor(bex, dey, dex, bey);
let m_ab = minor_mag(aex, bey, bex, aey);
let m_bc = minor_mag(bex, cey, cex, bey);
let m_cd = minor_mag(cex, dey, dex, cey);
let m_da = minor_mag(dex, aey, aex, dey);
let m_ac = minor_mag(aex, cey, cex, aey);
let m_bd = minor_mag(bex, dey, dex, bey);
let three_mag = |z1: f64, n1: f64, z2: f64, n2: f64, z3: f64, n3: f64| {
((z1.abs() * n1 + U_N) + (z2.abs() * n2 + U_N)) + (z3.abs() * n3 + U_N)
};
let abc = aez * bc - bez * ac + cez * ab;
let bcd = bez * cd - cez * bd + dez * bc;
let cda = cez * da + dez * ac + aez * cd;
let dab = dez * ab + aez * bd + bez * da;
let m_abc = three_mag(aez, m_bc, bez, m_ac, cez, m_ab);
let m_bcd = three_mag(bez, m_cd, cez, m_bd, dez, m_bc);
let m_cda = three_mag(cez, m_da, dez, m_ac, aez, m_cd);
let m_dab = three_mag(dez, m_ab, aez, m_bd, bez, m_da);
let lift = |x: f64, y: f64, z: f64| x * x + y * y + z * z;
let lift_mag = |x: f64, y: f64, z: f64| ((x * x + U_N) + (y * y + U_N)) + (z * z + U_N);
let alift = lift(aex, aey, aez);
let blift = lift(bex, bey, bez);
let clift = lift(cex, cey, cez);
let dlift = lift(dex, dey, dez);
let det = (dlift * abc - clift * dab) + (blift * cda - alift * bcd);
let t_a = lift_mag(aex, aey, aez) * m_bcd + U_N;
let t_b = lift_mag(bex, bey, bez) * m_cda + U_N;
let t_c = lift_mag(cex, cey, cez) * m_dab + U_N;
let t_d = lift_mag(dex, dey, dez) * m_abc + U_N;
let bound = ISP_ERR_BOUND_A * ((t_d + t_c) + (t_b + t_a)) + U_S;
(det, bound)
}
fn insphere_stage1(
pa: [f64; 3],
pb: [f64; 3],
pc: [f64; 3],
pd: [f64; 3],
pe: [f64; 3],
) -> (f64, f64) {
let aex = pa[0] - pe[0];
let aey = pa[1] - pe[1];
let aez = pa[2] - pe[2];
let bex = pb[0] - pe[0];
let bey = pb[1] - pe[1];
let bez = pb[2] - pe[2];
let cex = pc[0] - pe[0];
let cey = pc[1] - pe[1];
let cez = pc[2] - pe[2];
let dex = pd[0] - pe[0];
let dey = pd[1] - pe[1];
let dez = pd[2] - pe[2];
let ab = aex * bey - bex * aey;
let bc = bex * cey - cex * bey;
let cd = cex * dey - dex * cey;
let da = dex * aey - aex * dey;
let ac = aex * cey - cex * aey;
let bd = bex * dey - dex * bey;
let abc = aez * bc - bez * ac + cez * ab;
let bcd = bez * cd - cez * bd + dez * bc;
let cda = cez * da + dez * ac + aez * cd;
let dab = dez * ab + aez * bd + bez * da;
let alift = aex * aex + aey * aey + aez * aez;
let blift = bex * bex + bey * bey + bez * bez;
let clift = cex * cex + cey * cey + cez * cez;
let dlift = dex * dex + dey * dey + dez * dez;
let det = (dlift * abc - clift * dab) + (blift * cda - alift * bcd);
let abc_p = aez.abs() * (bex.abs() * cey.abs() + cex.abs() * bey.abs())
+ bez.abs() * (aex.abs() * cey.abs() + cex.abs() * aey.abs())
+ cez.abs() * (aex.abs() * bey.abs() + bex.abs() * aey.abs());
let bcd_p = bez.abs() * (cex.abs() * dey.abs() + dex.abs() * cey.abs())
+ cez.abs() * (bex.abs() * dey.abs() + dex.abs() * bey.abs())
+ dez.abs() * (bex.abs() * cey.abs() + cex.abs() * bey.abs());
let cda_p = cez.abs() * (dex.abs() * aey.abs() + aex.abs() * dey.abs())
+ dez.abs() * (aex.abs() * cey.abs() + cex.abs() * aey.abs())
+ aez.abs() * (cex.abs() * dey.abs() + dex.abs() * cey.abs());
let dab_p = dez.abs() * (aex.abs() * bey.abs() + bex.abs() * aey.abs())
+ aez.abs() * (bex.abs() * dey.abs() + dex.abs() * bey.abs())
+ bez.abs() * (dex.abs() * aey.abs() + aex.abs() * dey.abs());
let permanent =
alift.abs() * bcd_p + blift.abs() * cda_p + clift.abs() * dab_p + dlift.abs() * abc_p;
(det, permanent)
}
#[must_use = "the insphere result is the return value; discarding it is almost certainly a bug"]
pub fn insphere(
pa: [f64; 3],
pb: [f64; 3],
pc: [f64; 3],
pd: [f64; 3],
pe: [f64; 3],
) -> Result<Sign, NonFinite> {
if !pa
.iter()
.chain(&pb)
.chain(&pc)
.chain(&pd)
.chain(&pe)
.all(|v| v.is_finite())
{
counters::record(Predicate::Insphere, Outcome::NonFinite);
return Err(NonFinite::new());
}
let (det_raw, bound_raw) = insphere_filter_raw(pa, pb, pc, pd, pe);
if det_raw > bound_raw || -det_raw > bound_raw {
counters::record(Predicate::Insphere, Outcome::Fast);
return Ok(sign_of(det_raw));
}
let coords = [
pa[0], pa[1], pa[2], pb[0], pb[1], pb[2], pc[0], pc[1], pc[2], pd[0], pd[1], pd[2], pe[0],
pe[1], pe[2],
];
let (sa, sb, sc, sd, se) = match prepare(&coords, Window::INSPHERE) {
Prepared::AsIs => (pa, pb, pc, pd, pe),
Prepared::Scaled(s) => {
let scale = |p: [f64; 3]| [p[0] * s, p[1] * s, p[2] * s];
(scale(pa), scale(pb), scale(pc), scale(pd), scale(pe))
}
Prepared::OutOfWindow => return Ok(insphere_exact_integer(pa, pb, pc, pd, pe)),
};
let (det, permanent) = insphere_stage1(sa, sb, sc, sd, se);
if det > ISP_ERR_BOUND_A * permanent || -det > ISP_ERR_BOUND_A * permanent {
counters::record(Predicate::Insphere, Outcome::Fast);
return Ok(sign_of(det));
}
let result = insphere_adapt(sa, sb, sc, sd, se, permanent);
Ok(if result == Sign::Zero {
insphere_exact_integer(pa, pb, pc, pd, pe)
} else {
counters::record(Predicate::Insphere, Outcome::Fast);
result
})
}
fn scale_by_exact_lift(e: &[f64], vx: f64, vy: f64, vz: f64, sign: f64, out: &mut [f64]) -> usize {
debug_assert!(e.len() <= 24, "lift scaling expects a 3x3 sub-determinant");
#[allow(clippy::float_cmp)]
{
debug_assert!(sign == 1.0 || sign == -1.0, "sign must be exactly +/-1");
}
debug_assert!(
out.len() >= 12 * e.len(),
"lift scaling output buffer too small"
);
let mut half = [0.0_f64; 48];
let mut sq_x = [0.0_f64; 96];
let mut sq_y = [0.0_f64; 96];
let mut sq_z = [0.0_f64; 96];
let n = scale_expansion(e, vx, &mut half);
let x_len = scale_expansion(&half[..n], sign * vx, &mut sq_x);
let n = scale_expansion(e, vy, &mut half);
let y_len = scale_expansion(&half[..n], sign * vy, &mut sq_y);
let n = scale_expansion(e, vz, &mut half);
let z_len = scale_expansion(&half[..n], sign * vz, &mut sq_z);
let mut xy = [0.0_f64; 192];
let xy_len = expansion_sum(&sq_x[..x_len], &sq_y[..y_len], &mut xy);
expansion_sum(&xy[..xy_len], &sq_z[..z_len], out)
}
#[inline]
fn expansion_sign(e: &[f64]) -> Sign {
match e.last() {
None => Sign::Zero,
Some(&top) => sign_of(top),
}
}
fn insphere_sub_determinants(
a: [f64; 3],
b: [f64; 3],
c: [f64; 3],
d: [f64; 3],
) -> [([f64; 24], usize); 4] {
let (aex, aey, aez) = (a[0], a[1], a[2]);
let (bex, bey, bez) = (b[0], b[1], b[2]);
let (cex, cey, cez) = (c[0], c[1], c[2]);
let (dex, dey, dez) = (d[0], d[1], d[2]);
let (axby1, axby0) = two_product(aex, bey);
let (bxay1, bxay0) = two_product(bex, aey);
let ab = two_two_diff(axby1, axby0, bxay1, bxay0);
let (bxcy1, bxcy0) = two_product(bex, cey);
let (cxby1, cxby0) = two_product(cex, bey);
let bc = two_two_diff(bxcy1, bxcy0, cxby1, cxby0);
let (cxdy1, cxdy0) = two_product(cex, dey);
let (dxcy1, dxcy0) = two_product(dex, cey);
let cd = two_two_diff(cxdy1, cxdy0, dxcy1, dxcy0);
let (dxay1, dxay0) = two_product(dex, aey);
let (axdy1, axdy0) = two_product(aex, dey);
let da = two_two_diff(dxay1, dxay0, axdy1, axdy0);
let (axcy1, axcy0) = two_product(aex, cey);
let (cxay1, cxay0) = two_product(cex, aey);
let ac = two_two_diff(axcy1, axcy0, cxay1, cxay0);
let (bxdy1, bxdy0) = two_product(bex, dey);
let (dxby1, dxby0) = two_product(dex, bey);
let bd = two_two_diff(bxdy1, bxdy0, dxby1, dxby0);
let mut t8a = [0.0_f64; 8];
let mut t8b = [0.0_f64; 8];
let mut t16 = [0.0_f64; 16];
let mut build = |p: &[f64; 4], sp: f64, q: &[f64; 4], sq: f64, r: &[f64; 4], sr: f64| {
let mut out = [0.0_f64; 24];
let n = scale_expansion(p, sp, &mut t8a);
let m = scale_expansion(q, sq, &mut t8b);
let nm = expansion_sum(&t8a[..n], &t8b[..m], &mut t16);
let n = scale_expansion(r, sr, &mut t8a);
let len = expansion_sum(&t16[..nm], &t8a[..n], &mut out);
(out, len)
};
[
build(&bc, aez, &ac, -bez, &ab, cez),
build(&cd, bez, &bd, -cez, &bc, dez),
build(&da, cez, &ac, dez, &cd, aez),
build(&ab, dez, &bd, aez, &da, bez),
]
}
fn insphere_adapt(
pa: [f64; 3],
pb: [f64; 3],
pc: [f64; 3],
pd: [f64; 3],
pe: [f64; 3],
permanent: f64,
) -> Sign {
let aex = pa[0] - pe[0];
let aey = pa[1] - pe[1];
let aez = pa[2] - pe[2];
let bex = pb[0] - pe[0];
let bey = pb[1] - pe[1];
let bez = pb[2] - pe[2];
let cex = pc[0] - pe[0];
let cey = pc[1] - pe[1];
let cez = pc[2] - pe[2];
let dex = pd[0] - pe[0];
let dey = pd[1] - pe[1];
let dez = pd[2] - pe[2];
let [
(abc_exp, abc_len),
(bcd_exp, bcd_len),
(cda_exp, cda_len),
(dab_exp, dab_len),
] = insphere_sub_determinants(
[aex, aey, aez],
[bex, bey, bez],
[cex, cey, cez],
[dex, dey, dez],
);
let mut a288 = [0.0_f64; 288];
let mut b288 = [0.0_f64; 288];
let mut c288 = [0.0_f64; 288];
let mut d288 = [0.0_f64; 288];
let a_len = scale_by_exact_lift(&abc_exp[..abc_len], dex, dey, dez, 1.0, &mut a288);
let b_len = scale_by_exact_lift(&dab_exp[..dab_len], cex, cey, cez, -1.0, &mut b288);
let c_len = scale_by_exact_lift(&cda_exp[..cda_len], bex, bey, bez, 1.0, &mut c288);
let d_len = scale_by_exact_lift(&bcd_exp[..bcd_len], aex, aey, aez, -1.0, &mut d288);
let mut fin = [0.0_f64; 1152];
let mut t576a = [0.0_f64; 576];
let mut t576b = [0.0_f64; 576];
let ab_len = expansion_sum(&a288[..a_len], &b288[..b_len], &mut t576a);
let cd_len = expansion_sum(&c288[..c_len], &d288[..d_len], &mut t576b);
let fin_len = expansion_sum(&t576a[..ab_len], &t576b[..cd_len], &mut fin);
debug_assert!(
fin_len <= fin.len(),
"Stage 2 expansion length must stay within its buffer"
);
let det: f64 = fin[..fin_len].iter().sum();
if det.abs() >= ISP_ERR_BOUND_B * permanent {
return sign_of(det);
}
let aextail = two_diff_tail(pa[0], pe[0], aex);
let aeytail = two_diff_tail(pa[1], pe[1], aey);
let aeztail = two_diff_tail(pa[2], pe[2], aez);
let bextail = two_diff_tail(pb[0], pe[0], bex);
let beytail = two_diff_tail(pb[1], pe[1], bey);
let beztail = two_diff_tail(pb[2], pe[2], bez);
let cextail = two_diff_tail(pc[0], pe[0], cex);
let ceytail = two_diff_tail(pc[1], pe[1], cey);
let ceztail = two_diff_tail(pc[2], pe[2], cez);
let dextail = two_diff_tail(pd[0], pe[0], dex);
let deytail = two_diff_tail(pd[1], pe[1], dey);
let deztail = two_diff_tail(pd[2], pe[2], dez);
if aextail == 0.0
&& aeytail == 0.0
&& aeztail == 0.0
&& bextail == 0.0
&& beytail == 0.0
&& beztail == 0.0
&& cextail == 0.0
&& ceytail == 0.0
&& ceztail == 0.0
&& dextail == 0.0
&& deytail == 0.0
&& deztail == 0.0
{
return expansion_sign(&fin[..fin_len]);
}
#[cfg(test)]
INSPHERE_EXACT_COUNT.fetch_add(1, Ordering::Relaxed);
Sign::Zero
}
fn insphere_exact_integer(
pa: [f64; 3],
pb: [f64; 3],
pc: [f64; 3],
pd: [f64; 3],
pe: [f64; 3],
) -> Sign {
counters::record(Predicate::Insphere, Outcome::Exact);
use crate::exact_int::f64_to_insphere_int;
let ax = f64_to_insphere_int(pa[0]);
let ay = f64_to_insphere_int(pa[1]);
let az = f64_to_insphere_int(pa[2]);
let bx = f64_to_insphere_int(pb[0]);
let by_ = f64_to_insphere_int(pb[1]);
let bz = f64_to_insphere_int(pb[2]);
let cx = f64_to_insphere_int(pc[0]);
let cy = f64_to_insphere_int(pc[1]);
let cz = f64_to_insphere_int(pc[2]);
let dx = f64_to_insphere_int(pd[0]);
let dy = f64_to_insphere_int(pd[1]);
let dz = f64_to_insphere_int(pd[2]);
let ex = f64_to_insphere_int(pe[0]);
let ey = f64_to_insphere_int(pe[1]);
let ez = f64_to_insphere_int(pe[2]);
let aex = ax - ex;
let aey = ay - ey;
let aez = az - ez;
let bex = bx - ex;
let bey = by_ - ey;
let bez = bz - ez;
let cex = cx - ex;
let cey = cy - ey;
let cez = cz - ez;
let dex = dx - ex;
let dey = dy - ey;
let dez = dz - ez;
let alift = aex * aex + aey * aey + aez * aez;
let blift = bex * bex + bey * bey + bez * bez;
let clift = cex * cex + cey * cey + cez * cez;
let dlift = dex * dex + dey * dey + dez * dez;
let ab = aex * bey - bex * aey;
let bc = bex * cey - cex * bey;
let cd = cex * dey - dex * cey;
let da = dex * aey - aex * dey;
let ac = aex * cey - cex * aey;
let bd = bex * dey - dex * bey;
let abc = aez * bc - bez * ac + cez * ab;
let bcd = bez * cd - cez * bd + dez * bc;
let cda = cez * da + dez * ac + aez * cd;
let dab = dez * ab + aez * bd + bez * da;
let det = dlift * abc - clift * dab + blift * cda - alift * bcd;
if det.is_positive() {
Sign::Positive
} else if det.is_negative() {
Sign::Negative
} else {
counters::record(Predicate::Insphere, Outcome::ExactZero);
Sign::Zero
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_possible_wrap,
clippy::range_plus_one,
clippy::no_effect_underscore_binding
)]
mod tests {
use super::*;
use core::sync::atomic::Ordering;
fn reset_exact_count() {
INSPHERE_EXACT_COUNT.store(0, Ordering::SeqCst);
}
#[test]
fn insphere_clearly_inside() {
let pa = [1.0_f64, 0.0, 0.0];
let pb = [-1.0_f64 / 3.0, 2.0 * (2.0_f64).sqrt() / 3.0, 0.0];
let pc = [
-1.0_f64 / 3.0,
-(2.0_f64).sqrt() / 3.0,
(2.0_f64 / 3.0_f64).sqrt(),
];
let pd = [
-1.0_f64 / 3.0,
-(2.0_f64).sqrt() / 3.0,
-(2.0_f64 / 3.0_f64).sqrt(),
];
let pe = [0.0_f64, 0.0, 0.0];
assert_eq!(crate::orient3d(pa, pb, pc, pd), Ok(Sign::Positive));
assert_eq!(insphere(pa, pb, pc, pd, pe), Ok(Sign::Positive));
}
#[test]
fn insphere_clearly_outside() {
let pa = [1.0_f64, 0.0, 0.0];
let pb = [0.0_f64, 1.0, 0.0];
let pc = [0.0_f64, 0.0, 1.0];
let pd = [-1.0_f64, 0.0, 0.0];
let pe = [10.0_f64, 10.0, 10.0];
assert_eq!(crate::orient3d(pa, pb, pc, pd), Ok(Sign::Positive));
assert_eq!(insphere(pa, pb, pc, pd, pe), Ok(Sign::Negative));
}
#[test]
fn insphere_exactly_on_circumsphere() {
let pa = [1.0_f64, 0.0, 0.0];
let pb = [0.0_f64, 1.0, 0.0];
let pc = [0.0_f64, 0.0, 1.0];
let pd = [-1.0_f64, 0.0, 0.0];
let pe = [0.0_f64, -1.0, 0.0];
assert_eq!(crate::orient3d(pa, pb, pc, pd), Ok(Sign::Positive));
assert_eq!(insphere(pa, pb, pc, pd, pe), Ok(Sign::Zero));
}
#[test]
fn insphere_coplanar_reference_points_are_zero() {
let pa = [1.0_f64, 0.0, 0.0];
let pb = [-1.0_f64, 0.0, 0.0];
let pc = [0.0_f64, 1.0, 0.0];
let pd = [0.0_f64, -1.0, 0.0];
let pe = [0.0_f64, 0.0, 1.0];
assert_eq!(crate::orient3d(pa, pb, pc, pd), Ok(Sign::Zero));
assert_eq!(insphere(pa, pb, pc, pd, pe), Ok(Sign::Zero));
}
#[test]
fn insphere_point_just_outside() {
let pa = [1.0_f64, 0.0, 0.0];
let pb = [0.0_f64, 1.0, 0.0];
let pc = [0.0_f64, 0.0, 1.0];
let pd = [-1.0_f64, 0.0, 0.0];
let pe = [1.0_f64 + 1e-13, 0.0, 0.0];
assert_eq!(insphere(pa, pb, pc, pd, pe), Ok(Sign::Negative));
}
#[test]
fn insphere_exact_path_fires_on_cospherical_input() {
reset_exact_count();
let (a, b, c) = (1.0_f64 / 3.0, 1.0_f64 / 7.0, 1.0_f64 / 11.0);
let pa = [a, b, c];
let pb = [b, c, a];
let pc = [c, a, b];
let pd = [-a, b, c];
let pe = [a, -b, -c];
assert_ne!(crate::orient3d(pa, pb, pc, pd), Ok(Sign::Zero));
assert_eq!(insphere(pa, pb, pc, pd, pe), Ok(Sign::Zero));
let count = INSPHERE_EXACT_COUNT.load(Ordering::SeqCst);
assert!(count > 0, "the exact stage must be reached (count {count})");
}
#[test]
fn insphere_non_finite_inputs_return_err() {
let inf = f64::INFINITY;
let nan = f64::NAN;
assert_eq!(
insphere(
[inf, 0.0, 0.0],
[0.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[0.0, 0.5, 0.5],
),
Err(NonFinite::new()),
"a +inf coordinate must be rejected"
);
assert_eq!(
insphere(
[nan, 0.0, 0.0],
[0.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[0.0, 0.5, 0.5],
),
Err(NonFinite::new()),
"a NaN coordinate must be rejected"
);
}
#[test]
fn insphere_finite_overflow_is_not_zero() {
let m = f64::MAX;
let result = insphere(
[m, m, m],
[m, 0.0, 0.0],
[0.0, m, 0.0],
[0.0, 0.0, m],
[-m, -m, -m],
);
assert_eq!(
result,
Ok(Sign::Negative),
"finite non-cospherical input must not collapse to Zero on overflow"
);
}
}