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