1use 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
81const FILTERED_BOUND: f64 = 256.0 * f64::EPSILON;
89
90const COMPENSATED_BOUND: f64 = 64.0 * f64::EPSILON;
92
93#[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 #[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 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 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 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 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#[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#[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 -ad2 * minor_a + bd2 * minor_b - cd2 * minor_c + dd2 * minor_d
256}
257
258#[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 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 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 -ad2 * minor_a + bd2 * minor_b - cd2 * minor_c + dd2 * minor_d
330}
331
332fn 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 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 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
356fn 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 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 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
426fn 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
458fn 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 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 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 let mut accum = [0.0f64; MAX_EXPANSION_INSPHERE];
499 let mut temp = [0.0f64; MAX_EXPANSION_INSPHERE];
500 let mut product = [0.0f64; 256]; let mut scratch = [0.0f64; MAX_EXPANSION_INSPHERE];
502 let mut scaled = [0.0f64; 32]; let mut accum_len = 0usize;
504
505 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); add_product!(bd2_exp, bd2_len, minor_b, mb_len, false); add_product!(cd2_exp, cd2_len, minor_c, mc_len, true); add_product!(dd2_exp, dd2_len, minor_d, md_len, false); 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
542pub 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#[cfg(test)]
579mod tests {
580 use super::*;
581 use crate::specialized_libs::computational_geometry::exact_test_helper::Exact;
582
583 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 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 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 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); 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); 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); assert_eq!(insphere(a, b, c, d, e), Sign::Zero);
716 }
717
718 #[test]
719 fn sign_flips_for_negative_orientation() {
720 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); let s = insphere(a, b, c, d, e);
729 assert_eq!(s, Sign::Negative);
731 }
732
733 #[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)), (a, b, c, d, Point3::new(2.0, 0.0, 0.0)), (a, b, c, d, Point3::new(0.0, -1.0, 0.0)), (a, b, c, d, Point3::new(0.5, 0.5, 0.5)), (
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 #[test]
763 fn cospherical_five_points() {
764 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 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 #[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); 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 #[test]
811 fn extreme_exponents_agree_with_exact() {
812 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 #[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); 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); 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 #[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 #[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 #[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 #[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); let _ = insphere(a, b, c, d, e);
940 }
941}