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}