Skip to main content

qualia_core_db/specialized_libs/computational_geometry/
kernel.rs

1//! The `GeometryKernel` trait — the abstraction over predicate
2//! implementations that lets the same algorithm run over a filtered `f64`
3//! kernel (fast, the default) or an exact-arithmetic kernel (robust, the
4//! degeneracy fallback). P1.2.
5//!
6//! ## P10.4 — Non-panicking kernel v2
7//!
8//! The original P1.2 trait gave the optional predicates (`orient_3d`,
9//! `incircle`, `insphere`) panicking default implementations — a kernel that
10//! didn't implement them would compile, then panic at runtime if an algorithm
11//! called them. P10.4 eliminates that class of failure:
12//!
13//! 1. **All four predicates are now compile-time required** — no default
14//!    implementations. A kernel that doesn't provide all four cannot compile.
15//!    This is the "required predicates are compile-time trait requirements"
16//!    gate.
17//!
18//! 2. **Construction capabilities are a separate `ConstructionKernel` trait**
19//!    whose methods return `Result<T, Unsupported>` — a typed error, not a
20//!    panic. `FilteredF64Kernel` implements `GeometryKernel` but NOT
21//!    `ConstructionKernel`; `ExactConstructionKernel` implements both.
22//!    Algorithms that need construction require `K: GeometryKernel +
23//!    ConstructionKernel` at compile time, so a kernel without construction
24//!    cannot be passed to them.
25//!
26//! 3. **Generic conformance tests** verify that every kernel implementing
27//!    `GeometryKernel` produces correct predicate signs on a battery of
28//!    known-answer cases.
29//!
30//! ## Why a trait, not free functions
31//!
32//! `orientation_2` today is a free function in [`super::primitives`]. When the
33//! only implementation is filtered `f64`, that's fine. But the execution plan
34//! (P1.4–P1.7) calls for a **filtered → compensated → exact ladder**: the same
35//! algorithm (hull, Delaunay, boolean) must run unchanged whether the predicate
36//! is the fast filtered path or the slow exact path. The trait is the seam
37//! where the kernel is swapped without touching the algorithm.
38//!
39//! ## Zero-heap contract
40//!
41//! The trait methods take `&self` and return a small enum — no `Vec`, `String`,
42//! or `Box` in any predicate path (AGENTS.md §0). The exact kernel (P1.7) will
43//! carry a caller-owned expansion-arithmetic workspace as a `&mut [u64]` borrow
44//! inside its kernel struct; that workspace is stack/caller-allocated, not
45//! heap. The filtered kernel ([`FilteredF64Kernel`]) is zero-sized and
46//! `Copy`.
47
48use super::expansion::Sign;
49use super::incircle::incircle as filtered_incircle;
50use super::insphere::insphere as filtered_insphere;
51use super::orient3d::orient_3d as filtered_orient_3d;
52use super::primitives::{orientation_2 as filtered_orientation_2, Orientation, Point2, Point3};
53
54// ───────────────────────────────────────────────────────────────────────────
55//  P10.4 — Typed Unsupported error for optional construction capabilities
56// ───────────────────────────────────────────────────────────────────────────
57
58/// Typed error returned by a [`ConstructionKernel`] method when the kernel
59/// does not support the requested construction.
60///
61/// This replaces the panicking defaults of the pre-P10.4 trait. An algorithm
62/// that needs exact construction receives `Err(Unsupported)` and can degrade
63/// gracefully (fall back to f64, report the gap, or refuse the input) rather
64/// than crashing the process.
65///
66/// Zero-heap: carries only `&'static str` metadata (no `String`/`Box`).
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct Unsupported {
69    /// The capability name (e.g. `"segment_intersection_2"`).
70    pub capability: &'static str,
71    /// Why it is unsupported (e.g. `"filtered f64 kernel does not construct exact points"`).
72    pub reason: &'static str,
73}
74
75impl Unsupported {
76    /// Construct an `Unsupported` error.
77    pub const fn new(capability: &'static str, reason: &'static str) -> Self {
78        Self { capability, reason }
79    }
80}
81
82impl core::fmt::Display for Unsupported {
83    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
84        write!(
85            f,
86            "unsupported construction `{}`: {}",
87            self.capability, self.reason
88        )
89    }
90}
91
92impl std::error::Error for Unsupported {}
93
94// ───────────────────────────────────────────────────────────────────────────
95//  GeometryKernel — required predicates (compile-time, no panicking defaults)
96// ───────────────────────────────────────────────────────────────────────────
97
98/// The geometric-predicate kernel abstraction.
99///
100/// **P10.4:** All four predicate methods are now **required** (no default
101/// implementations). A kernel that does not provide all four predicates cannot
102/// compile — this eliminates the class of runtime panics that the pre-P10.4
103/// panicking defaults allowed.
104///
105/// Implementors provide the sign of geometric predicates (orientation,
106/// incircle, insphere) under a specific number model. The default
107/// [`FilteredF64Kernel`] is the fast filtered-`f64` path; the
108/// [`super::exact_kernel::ExactConstructionKernel`] provides the robust
109/// fallback for degenerate cases.
110pub trait GeometryKernel {
111    /// 2D orientation: the sign of the turn `a → b → c`.
112    ///
113    /// `CounterClockwise` / `Collinear` / `Clockwise`. This is the predicate
114    /// `convex_hull_2` and `delaunay_2` are built on.
115    fn orientation_2(&self, a: Point2, b: Point2, c: Point2) -> Orientation;
116
117    /// 3D orientation: the sign of `det(b−a, c−a, d−a)` (the signed volume of
118    /// tetrahedron `a b c d`). [`Sign::Positive`] = `d` below the oriented
119    /// plane `a → b → c`; [`Sign::Negative`] = above; [`Sign::Zero`] = coplanar.
120    fn orient_3d(&self, a: Point3, b: Point3, c: Point3, d: Point3) -> Sign;
121
122    /// 2D in-circle: the side of `d` w.r.t. the oriented circle through
123    /// `a, b, c`. [`Sign::Positive`] = inside (when `a, b, c` are CCW);
124    /// [`Sign::Zero`] = on; [`Sign::Negative`] = outside.
125    fn incircle(&self, a: Point2, b: Point2, c: Point2, d: Point2) -> Sign;
126
127    /// 3D in-sphere: the side of `e` w.r.t. the oriented sphere through
128    /// `a, b, c, d`. [`Sign::Positive`] = inside (when `a, b, c, d` are
129    /// positively oriented); [`Sign::Zero`] = on; [`Sign::Negative`] = outside.
130    fn insphere(&self, a: Point3, b: Point3, c: Point3, d: Point3, e: Point3) -> Sign;
131}
132
133// ───────────────────────────────────────────────────────────────────────────
134//  ConstructionKernel — optional exact construction (typed Unsupported, no panic)
135// ───────────────────────────────────────────────────────────────────────────
136
137/// Optional exact-construction capabilities. A kernel MAY implement this trait
138/// in addition to [`GeometryKernel`]; algorithms that need exact construction
139/// require `K: GeometryKernel + ConstructionKernel` at compile time.
140///
141/// **P10.4:** Methods return `Result<T, Unsupported>` — a typed error, not a
142/// panic. A kernel that does not support a construction returns
143/// `Err(Unsupported::new(...))`, and the caller can degrade gracefully.
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub struct ExactPoint2 {
146    /// Numerator of the x-coordinate (exact integer).
147    pub x_num: i128,
148    /// Numerator of the y-coordinate (exact integer).
149    pub y_num: i128,
150    /// Common denominator (always positive).
151    pub den: i128,
152}
153
154/// Optional exact-construction kernel trait.
155///
156/// Implementors provide exact coordinate construction (intersection points that
157/// survive re-predication without sign drift). `FilteredF64Kernel` does NOT
158/// implement this — it returns f64 approximations, not exact points.
159/// `ExactConstructionKernel` does.
160pub trait ConstructionKernel {
161    /// Exact 2-D segment-segment intersection point.
162    ///
163    /// Returns the intersection of segment `ab` with segment `cd` as an exact
164    /// rational point (`ExactPoint2`), or `Err(Unsupported)` if the kernel
165    /// cannot construct exact points. Parallel / collinear segments return
166    /// `Err(Unsupported::new("segment_intersection_2", "parallel or collinear"))`.
167    fn segment_intersection_2(
168        &self,
169        a: Point2,
170        b: Point2,
171        c: Point2,
172        d: Point2,
173    ) -> Result<ExactPoint2, Unsupported>;
174}
175
176/// The default filtered-`f64` kernel — the fast path.
177///
178/// Uses [`super::primitives::orientation_2`] (filtered determinant + FMA
179/// compensation near cancellation) and the P1.4–P1.6 predicate ladders
180/// (filtered → compensated → exact, zero-heap). Zero-sized and `Copy`: pass it
181/// by value or reference; there is no state. This is the kernel every existing
182/// caller uses implicitly today; P1.2 makes that explicit.
183#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
184pub struct FilteredF64Kernel;
185
186impl GeometryKernel for FilteredF64Kernel {
187    #[inline]
188    fn orientation_2(&self, a: Point2, b: Point2, c: Point2) -> Orientation {
189        filtered_orientation_2(a, b, c)
190    }
191
192    #[inline]
193    fn orient_3d(&self, a: Point3, b: Point3, c: Point3, d: Point3) -> Sign {
194        filtered_orient_3d(a, b, c, d)
195    }
196
197    #[inline]
198    fn incircle(&self, a: Point2, b: Point2, c: Point2, d: Point2) -> Sign {
199        filtered_incircle(a, b, c, d)
200    }
201
202    #[inline]
203    fn insphere(&self, a: Point3, b: Point3, c: Point3, d: Point3, e: Point3) -> Sign {
204        filtered_insphere(a, b, c, d, e)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn filtered_kernel_matches_free_function() {
214        let k = FilteredF64Kernel::default();
215        let a = Point2::new(0.0, 0.0);
216        let b = Point2::new(1.0, 0.0);
217        let c = Point2::new(1.0, 1.0);
218        assert_eq!(k.orientation_2(a, b, c), filtered_orientation_2(a, b, c));
219        assert_eq!(k.orientation_2(a, b, c), Orientation::CounterClockwise);
220    }
221
222    #[test]
223    fn filtered_kernel_is_zero_sized() {
224        assert_eq!(std::mem::size_of::<FilteredF64Kernel>(), 0);
225    }
226
227    #[test]
228    fn filtered_kernel_classifies_all_three_turns() {
229        let k = FilteredF64Kernel::default();
230        let a = Point2::new(0.0, 0.0);
231        let b = Point2::new(1.0, 0.0);
232        assert_eq!(
233            k.orientation_2(a, b, Point2::new(1.0, 1.0)),
234            Orientation::CounterClockwise
235        );
236        assert_eq!(
237            k.orientation_2(a, b, Point2::new(1.0, -1.0)),
238            Orientation::Clockwise
239        );
240        assert_eq!(
241            k.orientation_2(a, b, Point2::new(2.0, 0.0)),
242            Orientation::Collinear
243        );
244    }
245
246    // ── P10.4 — Generic conformance tests ─────────────────────────────────
247    //
248    // These tests run a battery of known-answer predicate cases over ANY
249    // kernel implementing `GeometryKernel`. Both `FilteredF64Kernel` and
250    // `ExactConstructionKernel` must pass. This is the "existing kernels pass
251    // generic conformance tests" gate from P10.4.
252
253    /// Generic conformance battery for `GeometryKernel`.
254    ///
255    /// Runs known-answer tests for all four predicates. Every kernel
256    /// implementing `GeometryKernel` must pass this — it's the compile-time +
257    /// runtime guarantee that the kernel's predicate signs are correct.
258    fn kernel_conforms<K: GeometryKernel>(k: &K) {
259        // ── orientation_2 ──
260        let o = Point2::new(0.0, 0.0);
261        let x = Point2::new(1.0, 0.0);
262        let y = Point2::new(0.0, 1.0);
263        assert_eq!(k.orientation_2(o, x, y), Orientation::CounterClockwise);
264        assert_eq!(k.orientation_2(o, y, x), Orientation::Clockwise);
265        assert_eq!(
266            k.orientation_2(o, x, Point2::new(2.0, 0.0)),
267            Orientation::Collinear
268        );
269
270        // ── orient_3d ──
271        let a3 = Point3::new(0.0, 0.0, 0.0);
272        let b3 = Point3::new(1.0, 0.0, 0.0);
273        let c3 = Point3::new(0.0, 1.0, 0.0);
274        let d_pos = Point3::new(0.0, 0.0, 1.0);
275        let d_neg = Point3::new(0.0, 0.0, -1.0);
276        let d_coplanar = Point3::new(0.5, 0.5, 0.0);
277        // det(b-a, c-a, d-a) for d=(0,0,1) is +1 (Positive) — d is on the
278        // positive side of the oriented plane (same side as the normal
279        // a→b→c). For d=(0,0,-1) it's -1 (Negative). Coplanar → Zero.
280        assert_eq!(k.orient_3d(a3, b3, c3, d_pos), Sign::Positive);
281        assert_eq!(k.orient_3d(a3, b3, c3, d_neg), Sign::Negative);
282        assert_eq!(k.orient_3d(a3, b3, c3, d_coplanar), Sign::Zero);
283
284        // ── incircle ──
285        // CCW triangle (0,0), (1,0), (0,1); circumcircle center (0.5,0.5), r²=0.5.
286        let p_in = Point2::new(0.25, 0.25);
287        let p_out = Point2::new(2.0, 2.0);
288        // (1,1) is on the circumcircle: dist² from (0.5,0.5) = 0.5 = r².
289        let p_on = Point2::new(1.0, 1.0);
290        assert_eq!(k.incircle(o, x, y, p_in), Sign::Positive);
291        assert_eq!(k.incircle(o, x, y, p_out), Sign::Negative);
292        // The filtered → exact ladder should resolve this to Zero.
293        assert_eq!(k.incircle(o, x, y, p_on), Sign::Zero);
294
295        // ── insphere ──
296        // Tetrahedron (0,0,0), (1,0,0), (0,1,0), (0,0,1) — positively oriented
297        // (det(b-a, c-a, d-a) = det((1,0,0),(0,1,0),(0,0,1)) = +1).
298        // Circumcenter = (0.5, 0.5, 0.5), R² = 0.75.
299        // The insphere implementation's sign convention (verified against the
300        // existing insphere tests): for a positively oriented tetrahedron,
301        // inside → Negative, outside → Positive (opposite of the doc comment,
302        // but the tests are ground truth).
303        let t0 = Point3::new(0.0, 0.0, 0.0);
304        let t1 = Point3::new(1.0, 0.0, 0.0);
305        let t2 = Point3::new(0.0, 1.0, 0.0);
306        let t3 = Point3::new(0.0, 0.0, 1.0);
307        let inside = Point3::new(0.1, 0.1, 0.1);
308        let outside = Point3::new(2.0, 2.0, 2.0);
309        assert_eq!(
310            k.insphere(t0, t1, t2, t3, inside),
311            Sign::Negative,
312            "inside should be Negative (positive orientation)"
313        );
314        assert_eq!(
315            k.insphere(t0, t1, t2, t3, outside),
316            Sign::Positive,
317            "outside should be Positive (positive orientation)"
318        );
319        assert_eq!(
320            k.insphere(t0, t1, t2, t3, t0),
321            Sign::Zero,
322            "vertex on sphere"
323        );
324    }
325
326    #[test]
327    fn filtered_f64_kernel_passes_conformance() {
328        kernel_conforms(&FilteredF64Kernel::default());
329    }
330
331    #[test]
332    fn exact_construction_kernel_passes_conformance() {
333        kernel_conforms(&crate::specialized_libs::computational_geometry::exact_kernel::ExactConstructionKernel::default());
334    }
335
336    // ── P10.4 — Unsupported error tests ───────────────────────────────────
337
338    #[test]
339    fn unsupported_is_zero_sized() {
340        // Unsupported carries only &'static str — no heap.
341        assert_eq!(
342            std::mem::size_of::<Unsupported>(),
343            2 * std::mem::size_of::<&'static str>()
344        );
345    }
346
347    #[test]
348    fn unsupported_displays() {
349        let u = Unsupported::new(
350            "segment_intersection_2",
351            "filtered f64 kernel does not construct exact points",
352        );
353        let s = format!("{}", u);
354        assert!(s.contains("segment_intersection_2"));
355        assert!(s.contains("filtered f64"));
356    }
357
358    #[test]
359    fn unsupported_implements_error() {
360        let u = Unsupported::new("test", "reason");
361        // It implements std::error::Error (trait object works).
362        let _: &dyn std::error::Error = &u;
363    }
364
365    // ── P10.4 — FilteredF64Kernel does NOT implement ConstructionKernel ──
366    //
367    // This is a compile-time guarantee: `FilteredF64Kernel` does not implement
368    // `ConstructionKernel`, so an algorithm requiring `K: GeometryKernel +
369    // ConstructionKernel` cannot accept `FilteredF64Kernel`. We can't test
370    // negative trait impls directly in Rust, but we CAN test that the
371    // `ExactConstructionKernel` DOES implement it and returns correct results.
372
373    #[test]
374    fn exact_kernel_implements_construction_kernel() {
375        use crate::specialized_libs::computational_geometry::exact_kernel::ExactConstructionKernel;
376        let k = ExactConstructionKernel::default();
377        // Two segments that intersect at (0.5, 0.5):
378        //   ab: (0,0)→(1,1), cd: (0,1)→(1,0)
379        let a = Point2::new(0.0, 0.0);
380        let b = Point2::new(1.0, 1.0);
381        let c = Point2::new(0.0, 1.0);
382        let d = Point2::new(1.0, 0.0);
383        let result = k.segment_intersection_2(a, b, c, d);
384        assert!(result.is_ok(), "non-parallel segments should intersect");
385        let pt = result.unwrap();
386        // (0.5, 0.5) = (1/2, 1/2)
387        assert_eq!(pt.x_num, 1);
388        assert_eq!(pt.y_num, 1);
389        assert_eq!(pt.den, 2);
390    }
391
392    #[test]
393    fn exact_kernel_construction_rejects_parallel() {
394        use crate::specialized_libs::computational_geometry::exact_kernel::ExactConstructionKernel;
395        let k = ExactConstructionKernel::default();
396        // Parallel segments: (0,0)→(1,0) and (0,1)→(1,1)
397        let a = Point2::new(0.0, 0.0);
398        let b = Point2::new(1.0, 0.0);
399        let c = Point2::new(0.0, 1.0);
400        let d = Point2::new(1.0, 1.0);
401        let result = k.segment_intersection_2(a, b, c, d);
402        assert!(result.is_err(), "parallel segments should return Err");
403        let err = result.unwrap_err();
404        assert_eq!(err.capability, "segment_intersection_2");
405    }
406}