Skip to main content

la_stack/
exact.rs

1//! Exact determinant sign via arbitrary-precision rational arithmetic.
2//!
3//! This module is only compiled when the `"exact"` Cargo feature is enabled.
4//!
5//! # Architecture
6//!
7//! `det_sign_exact` uses a two-stage adaptive-precision approach inspired by
8//! Shewchuk's robust geometric predicates:
9//!
10//! 1. **Fast filter (D ≤ 4)**: compute `det_direct()` and a conservative error
11//!    bound. If `|det| > bound`, the f64 sign is provably correct — return
12//!    immediately without allocating.
13//! 2. **Exact fallback**: convert entries to `BigRational` and run the Bareiss
14//!    algorithm (fraction-free Gaussian elimination) for a guaranteed-correct
15//!    sign.
16
17use num_bigint::{BigInt, Sign};
18use num_rational::BigRational;
19
20use crate::matrix::Matrix;
21
22// ---------------------------------------------------------------------------
23// Error-bound constants for the f64 fast filter.
24//
25// Each constant bounds the absolute error of `det_direct()` relative to the
26// *permanent* (sum of absolute products in the Leibniz expansion).  The
27// constants are conservative over-estimates following Shewchuk's methodology;
28// over-estimating just means we fall through to Bareiss more often.
29// ---------------------------------------------------------------------------
30
31const EPS: f64 = f64::EPSILON; // 2^-52
32
33/// D=2: one f64 multiply + one FMA → 2 rounding events.
34const ERR_COEFF_2: f64 = 3.0 * EPS + 16.0 * EPS * EPS;
35
36/// D=3: three 2×2 FMA minors + nested FMA combination.
37const ERR_COEFF_3: f64 = 8.0 * EPS + 64.0 * EPS * EPS;
38
39/// D=4: six hoisted 2×2 minors → four 3×3 cofactors → FMA row combination.
40const ERR_COEFF_4: f64 = 12.0 * EPS + 128.0 * EPS * EPS;
41
42/// Conservative absolute error bound for `det_direct()`, or `None` for D ≥ 5.
43///
44/// Returns `Some(bound)` such that `|det_direct() - det_exact| ≤ bound`.
45fn det_errbound<const D: usize>(m: &Matrix<D>) -> Option<f64> {
46    match D {
47        0 | 1 => Some(0.0), // No arithmetic — result is exact.
48        2 => {
49            let r = &m.rows;
50            let permanent = (r[0][0] * r[1][1]).abs() + (r[0][1] * r[1][0]).abs();
51            Some(ERR_COEFF_2 * permanent)
52        }
53        3 => {
54            let r = &m.rows;
55            let pm00 = (r[1][1] * r[2][2]).abs() + (r[1][2] * r[2][1]).abs();
56            let pm01 = (r[1][0] * r[2][2]).abs() + (r[1][2] * r[2][0]).abs();
57            let pm02 = (r[1][0] * r[2][1]).abs() + (r[1][1] * r[2][0]).abs();
58            let permanent = r[0][2]
59                .abs()
60                .mul_add(pm02, r[0][1].abs().mul_add(pm01, r[0][0].abs() * pm00));
61            Some(ERR_COEFF_3 * permanent)
62        }
63        4 => {
64            let r = &m.rows;
65            // 2×2 minor permanents from rows 2–3.
66            let sp23 = (r[2][2] * r[3][3]).abs() + (r[2][3] * r[3][2]).abs();
67            let sp13 = (r[2][1] * r[3][3]).abs() + (r[2][3] * r[3][1]).abs();
68            let sp12 = (r[2][1] * r[3][2]).abs() + (r[2][2] * r[3][1]).abs();
69            let sp03 = (r[2][0] * r[3][3]).abs() + (r[2][3] * r[3][0]).abs();
70            let sp02 = (r[2][0] * r[3][2]).abs() + (r[2][2] * r[3][0]).abs();
71            let sp01 = (r[2][0] * r[3][1]).abs() + (r[2][1] * r[3][0]).abs();
72            // 3×3 cofactor permanents from row 1.
73            let pc0 = r[1][3]
74                .abs()
75                .mul_add(sp12, r[1][2].abs().mul_add(sp13, r[1][1].abs() * sp23));
76            let pc1 = r[1][3]
77                .abs()
78                .mul_add(sp02, r[1][2].abs().mul_add(sp03, r[1][0].abs() * sp23));
79            let pc2 = r[1][3]
80                .abs()
81                .mul_add(sp01, r[1][1].abs().mul_add(sp03, r[1][0].abs() * sp13));
82            let pc3 = r[1][2]
83                .abs()
84                .mul_add(sp01, r[1][1].abs().mul_add(sp02, r[1][0].abs() * sp12));
85            let permanent = r[0][3].abs().mul_add(
86                pc3,
87                r[0][2]
88                    .abs()
89                    .mul_add(pc2, r[0][1].abs().mul_add(pc1, r[0][0].abs() * pc0)),
90            );
91            Some(ERR_COEFF_4 * permanent)
92        }
93        _ => None,
94    }
95}
96
97/// Convert an `f64` to an exact `BigRational`.
98///
99/// Every finite `f64` is exactly representable as a rational number (`m × 2^e`),
100/// so this conversion is lossless.
101///
102/// # Panics
103/// Panics if `x` is NaN or infinite.
104fn f64_to_bigrational(x: f64) -> BigRational {
105    BigRational::from_float(x).expect("non-finite matrix entry in det_sign_exact")
106}
107
108/// Compute the exact determinant of a `D×D` matrix using the Bareiss algorithm
109/// (fraction-free Gaussian elimination) in `BigRational` arithmetic.
110///
111/// Returns the determinant as an exact `BigRational`.
112fn bareiss_det<const D: usize>(m: &Matrix<D>) -> BigRational {
113    if D == 0 {
114        return BigRational::from_integer(BigInt::from(1));
115    }
116    if D == 1 {
117        return f64_to_bigrational(m.rows[0][0]);
118    }
119
120    // Convert f64 entries to exact BigRational.
121    let mut a: Vec<Vec<BigRational>> = Vec::with_capacity(D);
122    for r in 0..D {
123        let mut row = Vec::with_capacity(D);
124        for c in 0..D {
125            row.push(f64_to_bigrational(m.rows[r][c]));
126        }
127        a.push(row);
128    }
129
130    let zero = BigRational::from_integer(BigInt::from(0));
131    let mut prev_pivot = BigRational::from_integer(BigInt::from(1));
132    let mut sign: i8 = 1;
133
134    for k in 0..D {
135        // Partial pivoting: find non-zero entry in column k at or below row k.
136        if a[k][k] == zero {
137            let mut found = false;
138            for i in (k + 1)..D {
139                if a[i][k] != zero {
140                    a.swap(k, i);
141                    sign = -sign;
142                    found = true;
143                    break;
144                }
145            }
146            if !found {
147                // Entire column below (and including) diagonal is zero → singular.
148                return zero;
149            }
150        }
151
152        // Bareiss elimination for rows below k.
153        for i in (k + 1)..D {
154            for j in (k + 1)..D {
155                // a[i][j] = (a[k][k] * a[i][j] - a[i][k] * a[k][j]) / prev_pivot
156                a[i][j] = (&a[k][k] * &a[i][j] - &a[i][k] * &a[k][j]) / &prev_pivot;
157            }
158            // Zero out the eliminated column entry (not needed for det, but keeps
159            // the matrix consistent for potential debugging).
160            a[i][k] = zero.clone();
161        }
162
163        prev_pivot = a[k][k].clone();
164    }
165
166    let det = &a[D - 1][D - 1];
167    if sign < 0 { -det } else { det.clone() }
168}
169
170impl<const D: usize> Matrix<D> {
171    /// Exact determinant sign using adaptive-precision arithmetic.
172    ///
173    /// Returns `1` if `det > 0`, `-1` if `det < 0`, and `0` if `det == 0` (singular).
174    ///
175    /// For D ≤ 4, a fast f64 filter is tried first: `det_direct()` is compared
176    /// against a conservative error bound derived from the matrix permanent.
177    /// If the f64 result clearly exceeds the bound, the sign is returned
178    /// immediately without allocating.  Otherwise (and always for D ≥ 5), the
179    /// Bareiss algorithm runs in exact [`BigRational`] arithmetic.
180    ///
181    /// # When to use
182    ///
183    /// Use this when the sign of the determinant must be correct regardless of
184    /// floating-point conditioning (e.g. geometric predicates on near-degenerate
185    /// configurations).  For well-conditioned matrices the fast filter resolves
186    /// the sign without touching `BigRational`, so the overhead is minimal.
187    ///
188    /// # Examples
189    /// ```
190    /// use la_stack::prelude::*;
191    ///
192    /// let m = Matrix::<3>::from_rows([
193    ///     [1.0, 2.0, 3.0],
194    ///     [4.0, 5.0, 6.0],
195    ///     [7.0, 8.0, 9.0],
196    /// ]);
197    /// // This matrix is singular (row 3 = row 1 + row 2 in exact arithmetic).
198    /// assert_eq!(m.det_sign_exact(), 0);
199    ///
200    /// assert_eq!(Matrix::<3>::identity().det_sign_exact(), 1);
201    /// ```
202    ///
203    /// # Panics
204    /// Panics if any matrix entry is NaN or infinite.
205    #[must_use]
206    pub fn det_sign_exact(&self) -> i8 {
207        // Stage 1: f64 fast filter for D ≤ 4.
208        if let (Some(det_f64), Some(err)) = (self.det_direct(), det_errbound(self)) {
209            assert!(
210                det_f64.is_finite(),
211                "non-finite matrix entry in det_sign_exact"
212            );
213            if det_f64 > err {
214                return 1;
215            }
216            if det_f64 < -err {
217                return -1;
218            }
219        }
220
221        // Stage 2: exact Bareiss fallback.
222        let det = bareiss_det(self);
223        match det.numer().sign() {
224            Sign::Plus => 1,
225            Sign::Minus => -1,
226            Sign::NoSign => 0,
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use crate::DEFAULT_PIVOT_TOL;
235
236    #[test]
237    fn det_sign_exact_d0_is_positive() {
238        assert_eq!(Matrix::<0>::zero().det_sign_exact(), 1);
239    }
240
241    #[test]
242    fn det_sign_exact_d1_positive() {
243        let m = Matrix::<1>::from_rows([[42.0]]);
244        assert_eq!(m.det_sign_exact(), 1);
245    }
246
247    #[test]
248    fn det_sign_exact_d1_negative() {
249        let m = Matrix::<1>::from_rows([[-3.5]]);
250        assert_eq!(m.det_sign_exact(), -1);
251    }
252
253    #[test]
254    fn det_sign_exact_d1_zero() {
255        let m = Matrix::<1>::from_rows([[0.0]]);
256        assert_eq!(m.det_sign_exact(), 0);
257    }
258
259    #[test]
260    fn det_sign_exact_identity_2d() {
261        assert_eq!(Matrix::<2>::identity().det_sign_exact(), 1);
262    }
263
264    #[test]
265    fn det_sign_exact_identity_3d() {
266        assert_eq!(Matrix::<3>::identity().det_sign_exact(), 1);
267    }
268
269    #[test]
270    fn det_sign_exact_identity_4d() {
271        assert_eq!(Matrix::<4>::identity().det_sign_exact(), 1);
272    }
273
274    #[test]
275    fn det_sign_exact_identity_5d() {
276        assert_eq!(Matrix::<5>::identity().det_sign_exact(), 1);
277    }
278
279    #[test]
280    fn det_sign_exact_singular_duplicate_rows() {
281        let m = Matrix::<3>::from_rows([
282            [1.0, 2.0, 3.0],
283            [4.0, 5.0, 6.0],
284            [1.0, 2.0, 3.0], // duplicate of row 0
285        ]);
286        assert_eq!(m.det_sign_exact(), 0);
287    }
288
289    #[test]
290    fn det_sign_exact_singular_linear_combination() {
291        // Row 2 = row 0 + row 1 in exact arithmetic.
292        let m = Matrix::<3>::from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [5.0, 7.0, 9.0]]);
293        assert_eq!(m.det_sign_exact(), 0);
294    }
295
296    #[test]
297    fn det_sign_exact_negative_det_row_swap() {
298        // Swapping two rows of the identity negates the determinant.
299        let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
300        assert_eq!(m.det_sign_exact(), -1);
301    }
302
303    #[test]
304    fn det_sign_exact_negative_det_known() {
305        // det([[1,2],[3,4]]) = 1*4 - 2*3 = -2
306        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
307        assert_eq!(m.det_sign_exact(), -1);
308    }
309
310    #[test]
311    fn det_sign_exact_agrees_with_det_for_spd() {
312        // SPD matrix → positive determinant.
313        let m = Matrix::<3>::from_rows([[4.0, 2.0, 0.0], [2.0, 5.0, 1.0], [0.0, 1.0, 3.0]]);
314        assert_eq!(m.det_sign_exact(), 1);
315        assert!(m.det(DEFAULT_PIVOT_TOL).unwrap() > 0.0);
316    }
317
318    /// Near-singular matrix with an exact perturbation.
319    ///
320    /// The base matrix `[[1,2,3],[4,5,6],[7,8,9]]` is exactly singular (rows in
321    /// arithmetic progression).  Adding `2^-50` to entry (0,0) makes
322    /// `det = 2^-50 × cofactor(0,0) = 2^-50 × (5×9 − 6×8) = −3 × 2^-50 < 0`.
323    /// Both f64 `det_direct()` and `det_sign_exact()` should agree here.
324    #[test]
325    fn det_sign_exact_near_singular_perturbation() {
326        let perturbation = f64::from_bits(0x3CD0_0000_0000_0000); // 2^-50
327        let m = Matrix::<3>::from_rows([
328            [1.0 + perturbation, 2.0, 3.0],
329            [4.0, 5.0, 6.0],
330            [7.0, 8.0, 9.0],
331        ]);
332        // Exact: det = perturbation × (5×9 − 6×8) = perturbation × (−3) < 0.
333        assert_eq!(m.det_sign_exact(), -1);
334    }
335
336    /// For D ≤ 4, well-conditioned matrices should hit the fast filter
337    /// and never allocate `BigRational`.  We can't directly observe this,
338    /// but we verify correctness for a range of known signs.
339    #[test]
340    fn det_sign_exact_fast_filter_positive_4x4() {
341        let m = Matrix::<4>::from_rows([
342            [2.0, 1.0, 0.0, 0.0],
343            [1.0, 3.0, 1.0, 0.0],
344            [0.0, 1.0, 4.0, 1.0],
345            [0.0, 0.0, 1.0, 5.0],
346        ]);
347        // SPD tridiagonal → positive det.
348        assert_eq!(m.det_sign_exact(), 1);
349    }
350
351    #[test]
352    fn det_sign_exact_fast_filter_negative_4x4() {
353        // Swap rows 0 and 1 of the above → negate det.
354        let m = Matrix::<4>::from_rows([
355            [1.0, 3.0, 1.0, 0.0],
356            [2.0, 1.0, 0.0, 0.0],
357            [0.0, 1.0, 4.0, 1.0],
358            [0.0, 0.0, 1.0, 5.0],
359        ]);
360        assert_eq!(m.det_sign_exact(), -1);
361    }
362
363    #[test]
364    fn det_sign_exact_subnormal_entries() {
365        // Subnormal f64 values should convert losslessly.
366        let tiny = 5e-324_f64; // smallest positive subnormal
367        assert!(tiny.is_subnormal());
368
369        let m = Matrix::<2>::from_rows([[tiny, 0.0], [0.0, tiny]]);
370        // det = tiny^2 > 0
371        assert_eq!(m.det_sign_exact(), 1);
372    }
373
374    #[test]
375    #[should_panic(expected = "non-finite matrix entry")]
376    fn det_sign_exact_panics_on_nan() {
377        let m = Matrix::<2>::from_rows([[f64::NAN, 0.0], [0.0, 1.0]]);
378        let _ = m.det_sign_exact();
379    }
380
381    #[test]
382    #[should_panic(expected = "non-finite matrix entry")]
383    fn det_sign_exact_panics_on_infinity() {
384        let m = Matrix::<2>::from_rows([[f64::INFINITY, 0.0], [0.0, 1.0]]);
385        let _ = m.det_sign_exact();
386    }
387
388    #[test]
389    #[should_panic(expected = "non-finite matrix entry")]
390    fn det_sign_exact_panics_on_nan_5x5() {
391        // D ≥ 5 bypasses the fast filter, exercising the bareiss_det path.
392        let mut m = Matrix::<5>::identity();
393        m.set(2, 3, f64::NAN);
394        let _ = m.det_sign_exact();
395    }
396
397    #[test]
398    #[should_panic(expected = "non-finite matrix entry")]
399    fn det_sign_exact_panics_on_infinity_5x5() {
400        let mut m = Matrix::<5>::identity();
401        m.set(0, 0, f64::INFINITY);
402        let _ = m.det_sign_exact();
403    }
404
405    #[test]
406    fn det_sign_exact_pivot_needed_5x5() {
407        // D ≥ 5 skips the fast filter → exercises Bareiss pivoting.
408        // Permutation matrix with a single swap (rows 0↔1) → det = −1.
409        let m = Matrix::<5>::from_rows([
410            [0.0, 1.0, 0.0, 0.0, 0.0],
411            [1.0, 0.0, 0.0, 0.0, 0.0],
412            [0.0, 0.0, 1.0, 0.0, 0.0],
413            [0.0, 0.0, 0.0, 1.0, 0.0],
414            [0.0, 0.0, 0.0, 0.0, 1.0],
415        ]);
416        assert_eq!(m.det_sign_exact(), -1);
417    }
418
419    #[test]
420    fn det_sign_exact_5x5_known() {
421        // det of a permutation matrix with two swaps = +1 (even permutation).
422        let m = Matrix::<5>::from_rows([
423            [0.0, 1.0, 0.0, 0.0, 0.0],
424            [1.0, 0.0, 0.0, 0.0, 0.0],
425            [0.0, 0.0, 0.0, 1.0, 0.0],
426            [0.0, 0.0, 1.0, 0.0, 0.0],
427            [0.0, 0.0, 0.0, 0.0, 1.0],
428        ]);
429        // Two transpositions → even permutation → det = +1
430        assert_eq!(m.det_sign_exact(), 1);
431    }
432
433    // -----------------------------------------------------------------------
434    // Direct tests for internal helpers (coverage of private functions)
435    // -----------------------------------------------------------------------
436
437    #[test]
438    fn det_errbound_d0_is_zero() {
439        assert_eq!(det_errbound(&Matrix::<0>::zero()), Some(0.0));
440    }
441
442    #[test]
443    fn det_errbound_d1_is_zero() {
444        assert_eq!(det_errbound(&Matrix::<1>::from_rows([[42.0]])), Some(0.0));
445    }
446
447    #[test]
448    fn det_errbound_d2_positive() {
449        let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
450        let bound = det_errbound(&m).unwrap();
451        assert!(bound > 0.0);
452        // bound = ERR_COEFF_2 * (|1*4| + |2*3|) = ERR_COEFF_2 * 10
453        assert!(ERR_COEFF_2.mul_add(-10.0, bound).abs() < 1e-30);
454    }
455
456    #[test]
457    fn det_errbound_d3_positive() {
458        let m = Matrix::<3>::identity();
459        let bound = det_errbound(&m).unwrap();
460        assert!(bound > 0.0);
461    }
462
463    #[test]
464    fn det_errbound_d4_positive() {
465        let m = Matrix::<4>::identity();
466        let bound = det_errbound(&m).unwrap();
467        assert!(bound > 0.0);
468    }
469
470    #[test]
471    fn det_errbound_d5_is_none() {
472        assert_eq!(det_errbound(&Matrix::<5>::identity()), None);
473    }
474
475    #[test]
476    fn bareiss_det_d0_is_one() {
477        let det = bareiss_det(&Matrix::<0>::zero());
478        assert_eq!(det, BigRational::from_integer(BigInt::from(1)));
479    }
480
481    #[test]
482    fn bareiss_det_d1_returns_entry() {
483        let det = bareiss_det(&Matrix::<1>::from_rows([[7.0]]));
484        assert_eq!(det, f64_to_bigrational(7.0));
485    }
486
487    #[test]
488    fn bareiss_det_d3_with_pivoting() {
489        // First column has zero on diagonal → exercises pivot swap + break.
490        let m = Matrix::<3>::from_rows([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
491        let det = bareiss_det(&m);
492        // det of this permutation matrix = -1
493        assert_eq!(det, BigRational::from_integer(BigInt::from(-1)));
494    }
495
496    #[test]
497    fn bareiss_det_singular_all_zeros_in_column() {
498        // Column 1 is all zeros below diagonal after elimination → singular.
499        let m = Matrix::<3>::from_rows([[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]);
500        let det = bareiss_det(&m);
501        assert_eq!(det, BigRational::from_integer(BigInt::from(0)));
502    }
503}