Skip to main content

qualia_core_db/specialized_libs/computational_geometry/
sos.rs

1//! P12.2 — Simulation of Simplicity (SoS) for deterministic degeneracy resolution.
2//!
3//! When an exact predicate returns `Sign::Zero`, the input is degenerate
4//! (e.g. four coplanar points in `orient_3d`). Algorithms that branch on the
5//! sign need a deterministic non-zero answer — otherwise the output depends
6//! on floating-point noise or iteration order, breaking reproducibility.
7//!
8//! **Simulation of Simplicity** (Edelsbrunner & Mücke, 1990) resolves this by
9//! symbolically perturbing the input so that all predicates return non-zero
10//! signs. The perturbation is infinitesimal (it does not change the topology
11//! of non-degenerate inputs) and deterministic (it depends only on the point
12//! ordering, not on memory layout or rounding).
13//!
14//! ## How it works
15//!
16//! For `orient_3d(a, b, c, d)`, the 4×4 determinant is:
17//!
18//! ```text
19//! D = | ax  ay  az  1 |
20//!     | bx  by  bz  1 |
21//!     | cx  cy  cz  1 |
22//!     | dx  dy  dz  1 |
23//! ```
24//!
25//! When `D = 0`, perturb each coordinate `M[i][j]` (for `j ∈ {x, y, z}`) by
26//! `ε^(2^(3i+j))`. The perturbed determinant `D(ε)` is a polynomial in `ε`.
27//! Since `D = 0`, the sign of `D(ε)` for infinitesimally small `ε > 0` is
28//! the sign of the **first non-zero coefficient** in the polynomial expansion,
29//! ordered by increasing power of `ε`.
30//!
31//! The first-order coefficients are the **cofactors** `C_{ij}` of the original
32//! matrix. Each cofactor is a 2D orientation test of the three points ≠ `i`,
33//! projected onto the coordinate plane ≠ `j`. The 12 cofactors are evaluated
34//! in order of increasing `ε` power:
35//!
36//! | Order | (i, j) | Power  | Cofactor                         |
37//! |-------|--------|--------|----------------------------------|
38//! |  1    | (0,0)  | 1      | +orient_2d(b_yz, c_yz, d_yz)    |
39//! |  2    | (0,1)  | 2      | −orient_2d(b_xz, c_xz, d_xz)    |
40//! |  3    | (0,2)  | 4      | +orient_2d(b_xy, c_xy, d_xy)    |
41//! |  4    | (1,0)  | 8      | −orient_2d(a_yz, c_yz, d_yz)    |
42//! |  5    | (1,1)  | 16     | +orient_2d(a_xz, c_xz, d_xz)    |
43//! |  6    | (1,2)  | 32     | −orient_2d(a_xy, c_xy, d_xy)    |
44//! |  7    | (2,0)  | 64     | +orient_2d(a_yz, b_yz, d_yz)    |
45//! |  8    | (2,1)  | 128    | −orient_2d(a_xz, b_xz, d_xz)    |
46//! |  9    | (2,2)  | 256    | +orient_2d(a_xy, b_xy, d_xy)    |
47//! | 10    | (3,0)  | 512    | −orient_2d(a_yz, b_yz, c_yz)    |
48//! | 11    | (3,1)  | 1024   | +orient_2d(a_xz, b_xz, c_xz)    |
49//! | 12    | (3,2)  | 2048   | −orient_2d(a_xy, b_xy, c_xy)    |
50//!
51//! If all 12 cofactors are zero (all four points are collinear or identical),
52//! the configuration is fully degenerate and we return `Sign::Positive` as a
53//! deterministic constant. This is consistent with the SoS principle: the
54//! perturbation guarantees a total order, and for fully degenerate inputs any
55//! consistent sign is valid.
56//!
57//! ## Zero-heap contract
58//!
59//! [`orient_3d_sos`] is a predicate: it takes `Point3` (Copy) and returns
60//! `Sign` (Copy). No `Vec`, `String`, or `Box`. The underlying
61//! [`orientation_2`] is also zero-heap. This is a Tier-1 hot-path operation.
62
63use super::expansion::Sign;
64use super::orient3d::orient_3d;
65use super::primitives::{orientation_2, Orientation, Point2, Point3};
66
67/// Convert `Orientation` to `Sign`.
68#[inline]
69fn orient_to_sign(o: Orientation) -> Sign {
70    match o {
71        Orientation::CounterClockwise => Sign::Positive,
72        Orientation::Collinear => Sign::Zero,
73        Orientation::Clockwise => Sign::Negative,
74    }
75}
76
77/// 3-D orientation with Simulation of Simplicity tie-breaking.
78///
79/// Computes `orient_3d(a, b, c, d)`. If the result is `Sign::Zero` (coplanar),
80/// applies the SoS perturbation scheme to return a deterministic non-zero
81/// sign. This function **never returns `Sign::Zero`**.
82///
83/// The SoS sign is determined by the first non-zero 2D-orientation cofactor
84/// in a fixed order of increasing symbolic-perturbation power (see the module
85/// documentation for the full table).
86///
87/// # Properties
88///
89/// - **Deterministic**: the same input always produces the same sign, regardless
90///   of platform, build, or memory layout.
91/// - **Antisymmetric**: swapping any two points flips the sign (the cofactor
92///   order is consistent with the permutation parity).
93/// - **Non-degenerate passthrough**: when `orient_3d` returns non-zero, that
94///   sign is returned directly — SoS only activates on exact zeros.
95///
96/// # Zero-heap
97///
98/// No allocations. Stack-only computation over `Point3` / `Point2` values.
99pub fn orient_3d_sos(a: Point3, b: Point3, c: Point3, d: Point3) -> Sign {
100    let sign = orient_3d(a, b, c, d);
101    if sign != Sign::Zero {
102        return sign;
103    }
104
105    // SoS: evaluate the 12 first-order cofactors in order of increasing ε power.
106    // Each cofactor is a 2D orientation of three of the four points, projected
107    // onto a coordinate plane, with a sign flip from the (-1)^(i+j) factor.
108
109    // Order 1: (i=0, j=0) power=1  → +orient_2d(b_yz, c_yz, d_yz)
110    let s = orient_to_sign(orientation_2(
111        Point2::new(b.y, b.z),
112        Point2::new(c.y, c.z),
113        Point2::new(d.y, d.z),
114    ));
115    if s != Sign::Zero {
116        return s;
117    }
118
119    // Order 2: (i=0, j=1) power=2  → -orient_2d(b_xz, c_xz, d_xz)
120    let s = orient_to_sign(orientation_2(
121        Point2::new(b.x, b.z),
122        Point2::new(c.x, c.z),
123        Point2::new(d.x, d.z),
124    ));
125    if s != Sign::Zero {
126        return s.flip();
127    }
128
129    // Order 3: (i=0, j=2) power=4  → +orient_2d(b_xy, c_xy, d_xy)
130    let s = orient_to_sign(orientation_2(
131        Point2::new(b.x, b.y),
132        Point2::new(c.x, c.y),
133        Point2::new(d.x, d.y),
134    ));
135    if s != Sign::Zero {
136        return s;
137    }
138
139    // Order 4: (i=1, j=0) power=8  → -orient_2d(a_yz, c_yz, d_yz)
140    let s = orient_to_sign(orientation_2(
141        Point2::new(a.y, a.z),
142        Point2::new(c.y, c.z),
143        Point2::new(d.y, d.z),
144    ));
145    if s != Sign::Zero {
146        return s.flip();
147    }
148
149    // Order 5: (i=1, j=1) power=16 → +orient_2d(a_xz, c_xz, d_xz)
150    let s = orient_to_sign(orientation_2(
151        Point2::new(a.x, a.z),
152        Point2::new(c.x, c.z),
153        Point2::new(d.x, d.z),
154    ));
155    if s != Sign::Zero {
156        return s;
157    }
158
159    // Order 6: (i=1, j=2) power=32 → -orient_2d(a_xy, c_xy, d_xy)
160    let s = orient_to_sign(orientation_2(
161        Point2::new(a.x, a.y),
162        Point2::new(c.x, c.y),
163        Point2::new(d.x, d.y),
164    ));
165    if s != Sign::Zero {
166        return s.flip();
167    }
168
169    // Order 7: (i=2, j=0) power=64 → +orient_2d(a_yz, b_yz, d_yz)
170    let s = orient_to_sign(orientation_2(
171        Point2::new(a.y, a.z),
172        Point2::new(b.y, b.z),
173        Point2::new(d.y, d.z),
174    ));
175    if s != Sign::Zero {
176        return s;
177    }
178
179    // Order 8: (i=2, j=1) power=128 → -orient_2d(a_xz, b_xz, d_xz)
180    let s = orient_to_sign(orientation_2(
181        Point2::new(a.x, a.z),
182        Point2::new(b.x, b.z),
183        Point2::new(d.x, d.z),
184    ));
185    if s != Sign::Zero {
186        return s.flip();
187    }
188
189    // Order 9: (i=2, j=2) power=256 → +orient_2d(a_xy, b_xy, d_xy)
190    let s = orient_to_sign(orientation_2(
191        Point2::new(a.x, a.y),
192        Point2::new(b.x, b.y),
193        Point2::new(d.x, d.y),
194    ));
195    if s != Sign::Zero {
196        return s;
197    }
198
199    // Order 10: (i=3, j=0) power=512 → -orient_2d(a_yz, b_yz, c_yz)
200    let s = orient_to_sign(orientation_2(
201        Point2::new(a.y, a.z),
202        Point2::new(b.y, b.z),
203        Point2::new(c.y, c.z),
204    ));
205    if s != Sign::Zero {
206        return s.flip();
207    }
208
209    // Order 11: (i=3, j=1) power=1024 → +orient_2d(a_xz, b_xz, c_xz)
210    let s = orient_to_sign(orientation_2(
211        Point2::new(a.x, a.z),
212        Point2::new(b.x, b.z),
213        Point2::new(c.x, c.z),
214    ));
215    if s != Sign::Zero {
216        return s;
217    }
218
219    // Order 12: (i=3, j=2) power=2048 → -orient_2d(a_xy, b_xy, c_xy)
220    let s = orient_to_sign(orientation_2(
221        Point2::new(a.x, a.y),
222        Point2::new(b.x, b.y),
223        Point2::new(c.x, c.y),
224    ));
225    if s != Sign::Zero {
226        return s.flip();
227    }
228
229    // All 12 cofactors are zero: the four points are collinear or identical.
230    // Return a deterministic constant. This is consistent with the SoS
231    // principle — the perturbation guarantees a total order, and for fully
232    // degenerate inputs any consistent sign is valid.
233    Sign::Positive
234}
235
236// ───────────────────────────────────────────────────────────────────────────
237//  Tests
238// ───────────────────────────────────────────────────────────────────────────
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    fn p(x: f64, y: f64, z: f64) -> Point3 {
245        Point3::new(x, y, z)
246    }
247
248    #[test]
249    fn sos_never_returns_zero() {
250        // Test a variety of degenerate and non-degenerate configurations.
251        let cases = [
252            // Non-degenerate: regular tetrahedron.
253            (
254                p(0.0, 0.0, 0.0),
255                p(1.0, 0.0, 0.0),
256                p(0.0, 1.0, 0.0),
257                p(0.0, 0.0, 1.0),
258            ),
259            // Coplanar: four points in z=0.
260            (
261                p(0.0, 0.0, 0.0),
262                p(1.0, 0.0, 0.0),
263                p(0.0, 1.0, 0.0),
264                p(1.0, 1.0, 0.0),
265            ),
266            // Coplanar: four points in z=1.
267            (
268                p(0.0, 0.0, 1.0),
269                p(1.0, 0.0, 1.0),
270                p(0.0, 1.0, 1.0),
271                p(1.0, 1.0, 1.0),
272            ),
273            // Collinear: four points on the x-axis.
274            (
275                p(0.0, 0.0, 0.0),
276                p(1.0, 0.0, 0.0),
277                p(2.0, 0.0, 0.0),
278                p(3.0, 0.0, 0.0),
279            ),
280            // Identical points.
281            (
282                p(1.0, 2.0, 3.0),
283                p(1.0, 2.0, 3.0),
284                p(1.0, 2.0, 3.0),
285                p(1.0, 2.0, 3.0),
286            ),
287            // Coplanar with a shared vertex.
288            (
289                p(0.0, 0.0, 0.0),
290                p(2.0, 0.0, 0.0),
291                p(0.0, 2.0, 0.0),
292                p(1.0, 1.0, 0.0),
293            ),
294        ];
295
296        for (a, b, c, d) in &cases {
297            let sign = orient_3d_sos(*a, *b, *c, *d);
298            assert!(
299                sign != Sign::Zero,
300                "SoS must never return Zero for ({a:?}, {b:?}, {c:?}, {d:?})"
301            );
302        }
303    }
304
305    #[test]
306    fn sos_non_degenerate_matches_orient_3d() {
307        // Non-degenerate cases: SoS should return the same sign as orient_3d.
308        let a = p(0.0, 0.0, 0.0);
309        let b = p(1.0, 0.0, 0.0);
310        let c = p(0.0, 1.0, 0.0);
311        let d = p(0.0, 0.0, 1.0);
312
313        assert_eq!(orient_3d_sos(a, b, c, d), orient_3d(a, b, c, d));
314        assert_eq!(orient_3d_sos(a, b, c, d), Sign::Positive);
315
316        // Flip d to the other side.
317        let d2 = p(0.0, 0.0, -1.0);
318        assert_eq!(orient_3d_sos(a, b, c, d2), orient_3d(a, b, c, d2));
319        assert_eq!(orient_3d_sos(a, b, c, d2), Sign::Negative);
320    }
321
322    #[test]
323    fn sos_coplanar_is_deterministic() {
324        // Four coplanar points in z=0. SoS should always return the same sign.
325        let a = p(0.0, 0.0, 0.0);
326        let b = p(1.0, 0.0, 0.0);
327        let c = p(0.0, 1.0, 0.0);
328        let d = p(1.0, 1.0, 0.0);
329
330        let s1 = orient_3d_sos(a, b, c, d);
331        let s2 = orient_3d_sos(a, b, c, d);
332        assert_eq!(s1, s2, "SoS must be deterministic");
333        assert_ne!(s1, Sign::Zero, "SoS must not return Zero for coplanar");
334    }
335
336    #[test]
337    fn sos_antisymmetric_swap_two_points() {
338        // Swapping two points should flip the sign.
339        // For non-degenerate: orient_3d(a,b,c,d) = -orient_3d(b,a,c,d).
340        let a = p(0.0, 0.0, 0.0);
341        let b = p(1.0, 0.0, 0.0);
342        let c = p(0.0, 1.0, 0.0);
343        let d = p(0.0, 0.0, 1.0);
344
345        let s1 = orient_3d_sos(a, b, c, d);
346        let s2 = orient_3d_sos(b, a, c, d);
347        assert_eq!(s1, s2.flip(), "swapping a,b should flip the sign");
348
349        let s3 = orient_3d_sos(a, c, b, d);
350        assert_eq!(s1, s3.flip(), "swapping b,c should flip the sign");
351
352        let s4 = orient_3d_sos(a, b, d, c);
353        assert_eq!(s1, s4.flip(), "swapping c,d should flip the sign");
354    }
355
356    #[test]
357    fn sos_coplanar_swap_is_deterministic() {
358        // For coplanar points, SoS does NOT guarantee antisymmetry
359        // (the perturbation is tied to row position, not point identity).
360        // But it must still be deterministic and non-zero.
361        let a = p(0.0, 0.0, 0.0);
362        let b = p(1.0, 0.0, 0.0);
363        let c = p(0.0, 1.0, 0.0);
364        let d = p(1.0, 1.0, 0.0);
365
366        let s1 = orient_3d_sos(a, b, c, d);
367        let s2 = orient_3d_sos(b, a, c, d);
368        assert_ne!(s1, Sign::Zero);
369        assert_ne!(s2, Sign::Zero);
370        // Deterministic: same call → same result.
371        assert_eq!(s1, orient_3d_sos(a, b, c, d));
372        assert_eq!(s2, orient_3d_sos(b, a, c, d));
373    }
374
375    #[test]
376    fn sos_coplanar_square_consistent() {
377        // Four corners of a unit square in z=0.
378        // The SoS sign should be consistent with the orientation structure.
379        let a = p(0.0, 0.0, 0.0);
380        let b = p(1.0, 0.0, 0.0);
381        let c = p(1.0, 1.0, 0.0);
382        let d = p(0.0, 1.0, 0.0);
383
384        // (a, b, c, d) — the first cofactor is orient_2d(b_yz, c_yz, d_yz).
385        // b_yz = (0, 0), c_yz = (1, 0), d_yz = (1, 0) — collinear in yz.
386        // Second cofactor: -orient_2d(b_xz, c_xz, d_xz).
387        // b_xz = (1, 0), c_xz = (1, 0), d_xz = (0, 0) — collinear in xz.
388        // Third cofactor: +orient_2d(b_xy, c_xy, d_xy).
389        // b_xy = (1, 0), c_xy = (1, 1), d_xy = (0, 1).
390        // orient_2d((1,0), (1,1), (0,1)) = CCW (positive).
391        let sign = orient_3d_sos(a, b, c, d);
392        assert_eq!(
393            sign,
394            Sign::Positive,
395            "square (a,b,c,d) should be Positive via 3rd cofactor"
396        );
397    }
398
399    #[test]
400    fn sos_collinear_returns_positive() {
401        // Four collinear points on x-axis — all cofactors are zero.
402        // The fallback should return Sign::Positive.
403        let a = p(0.0, 0.0, 0.0);
404        let b = p(1.0, 0.0, 0.0);
405        let c = p(2.0, 0.0, 0.0);
406        let d = p(3.0, 0.0, 0.0);
407
408        let sign = orient_3d_sos(a, b, c, d);
409        assert_eq!(
410            sign,
411            Sign::Positive,
412            "collinear fallback should return Positive"
413        );
414    }
415
416    #[test]
417    fn sos_identical_points_returns_positive() {
418        // All four points identical — fully degenerate.
419        let a = p(1.0, 2.0, 3.0);
420        let sign = orient_3d_sos(a, a, a, a);
421        assert_eq!(
422            sign,
423            Sign::Positive,
424            "identical points fallback should return Positive"
425        );
426    }
427
428    #[test]
429    fn sos_cyclic_permutation_flips_sign() {
430        // A 4-cycle (a,b,c,d) → (b,c,d,a) is 3 transpositions = odd permutation.
431        // The determinant sign flips. For non-degenerate cases, SoS passes
432        // through the actual sign, so this must hold.
433        let a = p(0.0, 0.0, 0.0);
434        let b = p(1.0, 0.0, 0.0);
435        let c = p(0.0, 1.0, 0.0);
436        let d = p(0.0, 0.0, 1.0);
437
438        let s1 = orient_3d_sos(a, b, c, d);
439        let s2 = orient_3d_sos(b, c, d, a);
440        assert_eq!(s1, s2.flip(), "4-cycle (odd permutation) should flip sign");
441    }
442
443    #[test]
444    fn sos_coplanar_cyclic_is_deterministic() {
445        // For coplanar points, SoS doesn't guarantee permutation invariance,
446        // but must be deterministic and non-zero.
447        let a = p(0.0, 0.0, 0.0);
448        let b = p(1.0, 0.0, 0.0);
449        let c = p(1.0, 1.0, 0.0);
450        let d = p(0.0, 1.0, 0.0);
451
452        let s1 = orient_3d_sos(a, b, c, d);
453        let s2 = orient_3d_sos(b, c, d, a);
454        assert_ne!(s1, Sign::Zero);
455        assert_ne!(s2, Sign::Zero);
456        assert_eq!(s1, orient_3d_sos(a, b, c, d), "must be deterministic");
457    }
458
459    #[test]
460    fn sos_three_identical_one_different() {
461        // Three identical points + one different. The cofactors involving
462        // the three identical points will be zero, but cofactors involving
463        // the different point and two of the identical ones may be non-zero.
464        let a = p(0.0, 0.0, 0.0);
465        let b = p(0.0, 0.0, 0.0);
466        let c = p(0.0, 0.0, 0.0);
467        let d = p(1.0, 0.0, 0.0);
468
469        let sign = orient_3d_sos(a, b, c, d);
470        assert_ne!(sign, Sign::Zero, "must not return Zero");
471
472        // All cofactors will be zero because any 3-point subset is collinear
473        // (three identical points, or two identical + one on x-axis).
474        // So this should hit the fallback.
475        assert_eq!(sign, Sign::Positive);
476    }
477}