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}